From 8a69e402dfb862384e7434981ded3325aac40b71 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:30:11 -0400 Subject: [PATCH 01/13] Update generated plugins from context-mill (mirror test) --- .../basic-integration-1.3-conclude.md | 38 -------------- .../basic-integration-1.3-conclude.md | 38 -------------- .../basic-integration-1.3-conclude.md | 38 -------------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../all/skills/integration-nuxt-3.6/SKILL.md | 50 ------------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../logs-datadog/references/debug-logs-mcp.md | 50 ------------------- .../logs-go/references/debug-logs-mcp.md | 50 ------------------- .../logs-java/references/debug-logs-mcp.md | 50 ------------------- .../logs-nextjs/references/debug-logs-mcp.md | 50 ------------------- .../logs-nodejs/references/debug-logs-mcp.md | 50 ------------------- .../logs-other/references/debug-logs-mcp.md | 50 ------------------- .../logs-python/references/debug-logs-mcp.md | 50 ------------------- .../references/debug-logs-mcp.md | 50 ------------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../integration/skills/nuxt-3.6/SKILL.md | 50 ------------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../references/basic-integration-1.0-begin.md | 43 ---------------- .../references/basic-integration-1.1-edit.md | 37 -------------- .../basic-integration-1.2-revise.md | 22 -------- .../basic-integration-1.3-conclude.md | 38 -------------- .../skills/all/references/debug-logs-mcp.md | 50 ------------------- .../datadog/references/debug-logs-mcp.md | 50 ------------------- .../skills/go/references/debug-logs-mcp.md | 50 ------------------- .../skills/java/references/debug-logs-mcp.md | 50 ------------------- .../nextjs/references/debug-logs-mcp.md | 50 ------------------- .../nodejs/references/debug-logs-mcp.md | 50 ------------------- .../skills/other/references/debug-logs-mcp.md | 50 ------------------- .../python/references/debug-logs-mcp.md | 50 ------------------- 262 files changed, 9452 deletions(-) delete mode 100644 skills/posthog/all/skills/integration-android/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-angular/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-django/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-django/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-django/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-django/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-expo/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-expo/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-expo/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-expo/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-flask/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-flask/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-flask/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-flask/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-laravel/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-laravel/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-laravel/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-laravel/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-3.6/SKILL.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-python/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-python/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-python/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-python/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-native/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-native/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-native/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-native/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-ruby/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-ruby/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-ruby/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-ruby/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-swift/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-swift/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-swift/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-swift/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/all/skills/logs-datadog/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-go/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-java/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-nextjs/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-nodejs/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-other/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/logs-python/references/debug-logs-mcp.md delete mode 100644 skills/posthog/all/skills/omnibus-instrument-logs/references/debug-logs-mcp.md delete mode 100644 skills/posthog/integration/skills/android/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/android/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/android/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/android/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/angular/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/angular/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/angular/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/angular/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/astro-static/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/astro-static/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/astro-static/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/astro-static/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/django/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/django/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/django/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/django/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/expo/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/expo/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/expo/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/expo/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/fastapi/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/fastapi/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/fastapi/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/fastapi/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/flask/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/flask/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/flask/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/flask/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/javascript_node/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/javascript_node/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/javascript_node/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/javascript_node/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/javascript_web/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/javascript_web/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/javascript_web/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/javascript_web/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/laravel/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/laravel/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/laravel/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/laravel/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/nuxt-3.6/SKILL.md delete mode 100644 skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/python/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/python/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/python/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/python/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-native/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-native/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-native/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-native/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/react-vite/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/react-vite/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/react-vite/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/react-vite/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/ruby/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/ruby/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/ruby/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/ruby/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/sveltekit/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/sveltekit/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/sveltekit/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/sveltekit/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/swift/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/swift/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/swift/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/swift/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/integration/skills/vue-3/references/basic-integration-1.0-begin.md delete mode 100644 skills/posthog/integration/skills/vue-3/references/basic-integration-1.1-edit.md delete mode 100644 skills/posthog/integration/skills/vue-3/references/basic-integration-1.2-revise.md delete mode 100644 skills/posthog/integration/skills/vue-3/references/basic-integration-1.3-conclude.md delete mode 100644 skills/posthog/logs/skills/all/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/datadog/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/go/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/java/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/nextjs/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/nodejs/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/other/references/debug-logs-mcp.md delete mode 100644 skills/posthog/logs/skills/python/references/debug-logs-mcp.md diff --git a/skills/posthog/all/skills/integration-android/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-android/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-android/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-angular/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-angular/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-angular/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-astro-hybrid/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-astro-hybrid/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-ssr/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-astro-ssr/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-astro-ssr/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-astro-static/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-django/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-django/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-django/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-django/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-django/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-django/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-django/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-django/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-django/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-django/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-django/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-django/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-expo/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-expo/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-expo/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-expo/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-expo/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-fastapi/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-flask/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-flask/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-flask/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-flask/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-flask/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-javascript_node/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-javascript_web/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-laravel/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-nextjs-app-router/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-nextjs-pages-router/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-3.6/SKILL.md b/skills/posthog/all/skills/integration-nuxt-3.6/SKILL.md deleted file mode 100644 index f5127b63..00000000 --- a/skills/posthog/all/skills/integration-nuxt-3.6/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: integration-nuxt-3.6 -description: PostHog integration for Nuxt versions 3.0 to 3.6 -metadata: - author: PostHog - version: 1.9.4 ---- - -# PostHog integration for Nuxt 3.6 - -This skill helps you add PostHog analytics to Nuxt 3.6 applications. - -## Workflow - -Follow these steps in order to complete the integration: - -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion - -## Reference files - -- `references/EXAMPLE.md` - Nuxt 3.6 example project code -- `references/nuxt-js-3-6.md` - Nuxt.js (v3.0 to v3.6) - docs -- `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion - -The example project shows the target implementation pattern. Consult the documentation for API details. - -## Key principles - -- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. -- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. -- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. - -## Framework guidelines - -_No specific framework guidelines._ - -## Identifying users - -Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. - -## Error tracking - -Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-nuxt-3.6/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-nuxt-4/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-python/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-python/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-python/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-python/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-python/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-python/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-python/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-python/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-python/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-python/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-python/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-python/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-native/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-6/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-data/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-declarative/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-react-router-7-framework/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-react-vite/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-ruby-on-rails/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-ruby/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-sveltekit/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-swift/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-swift/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-swift/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-swift/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-swift/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-tanstack-start/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.0-begin.md b/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.1-edit.md b/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.2-revise.md b/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.3-conclude.md b/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/all/skills/integration-vue-3/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-datadog/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-datadog/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-datadog/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-go/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-go/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-go/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-java/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-java/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-java/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-nextjs/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-nextjs/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-nextjs/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-nodejs/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-nodejs/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-nodejs/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-other/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-other/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-other/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/logs-python/references/debug-logs-mcp.md b/skills/posthog/all/skills/logs-python/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/logs-python/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/omnibus-instrument-logs/references/debug-logs-mcp.md b/skills/posthog/all/skills/omnibus-instrument-logs/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/all/skills/omnibus-instrument-logs/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/integration/skills/android/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/android/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/android/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/android/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/android/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/android/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/android/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/android/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/android/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/android/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/android/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/android/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/angular/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/angular/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/angular/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/angular/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/angular/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/angular/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/angular/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/angular/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/angular/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/angular/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/angular/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/angular/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/astro-hybrid/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/astro-ssr/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/astro-static/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/astro-static/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/astro-static/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/astro-static/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/astro-static/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/astro-view-transitions/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/django/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/django/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/django/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/django/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/django/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/django/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/django/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/django/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/django/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/django/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/django/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/django/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/expo/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/expo/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/expo/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/expo/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/expo/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/expo/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/expo/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/expo/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/expo/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/expo/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/expo/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/expo/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/fastapi/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/fastapi/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/fastapi/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/fastapi/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/fastapi/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/flask/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/flask/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/flask/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/flask/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/flask/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/flask/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/flask/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/flask/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/flask/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/flask/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/flask/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/flask/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/javascript_node/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/javascript_web/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/laravel/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/laravel/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/laravel/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/laravel/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/laravel/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/laravel/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/laravel/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/laravel/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/laravel/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/laravel/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/laravel/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/laravel/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/nextjs-app-router/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/nextjs-pages-router/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-3.6/SKILL.md b/skills/posthog/integration/skills/nuxt-3.6/SKILL.md deleted file mode 100644 index f5127b63..00000000 --- a/skills/posthog/integration/skills/nuxt-3.6/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: integration-nuxt-3.6 -description: PostHog integration for Nuxt versions 3.0 to 3.6 -metadata: - author: PostHog - version: 1.9.4 ---- - -# PostHog integration for Nuxt 3.6 - -This skill helps you add PostHog analytics to Nuxt 3.6 applications. - -## Workflow - -Follow these steps in order to complete the integration: - -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion - -## Reference files - -- `references/EXAMPLE.md` - Nuxt 3.6 example project code -- `references/nuxt-js-3-6.md` - Nuxt.js (v3.0 to v3.6) - docs -- `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion - -The example project shows the target implementation pattern. Consult the documentation for API details. - -## Key principles - -- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. -- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. -- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. - -## Framework guidelines - -_No specific framework guidelines._ - -## Identifying users - -Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. - -## Error tracking - -Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/nuxt-3.6/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/nuxt-4/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/python/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/python/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/python/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/python/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/python/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/python/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/python/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/python/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/python/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/python/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/python/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/python/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-native/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-native/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-native/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-native/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-native/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-native/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-native/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-native/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-native/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-native/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-native/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-native/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-react-router-6/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-data/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-declarative/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-react-router-7-framework/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-code-based/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-tanstack-router-file-based/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/react-vite/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/react-vite/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/react-vite/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/react-vite/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/react-vite/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/ruby-on-rails/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/ruby/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/ruby/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/ruby/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/ruby/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/ruby/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/ruby/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/ruby/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/ruby/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/ruby/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/sveltekit/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/swift/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/swift/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/swift/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/swift/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/swift/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/swift/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/swift/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/swift/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/swift/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/swift/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/swift/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/swift/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/tanstack-start/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.0-begin.md b/skills/posthog/integration/skills/vue-3/references/basic-integration-1.0-begin.md deleted file mode 100644 index a97bbe7e..00000000 --- a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - - - Payment/checkout completion - - Webhook handlers - - Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.1-edit.md b/skills/posthog/integration/skills/vue-3/references/basic-integration-1.1-edit.md deleted file mode 100644 index ca9d70e6..00000000 --- a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.2-revise.md b/skills/posthog/integration/skills/vue-3/references/basic-integration-1.2-revise.md deleted file mode 100644 index 5ac72f06..00000000 --- a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) \ No newline at end of file diff --git a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.3-conclude.md b/skills/posthog/integration/skills/vue-3/references/basic-integration-1.3-conclude.md deleted file mode 100644 index b48af6a8..00000000 --- a/skills/posthog/integration/skills/vue-3/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics" based on the events created here. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/skills/posthog/logs/skills/all/references/debug-logs-mcp.md b/skills/posthog/logs/skills/all/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/all/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/datadog/references/debug-logs-mcp.md b/skills/posthog/logs/skills/datadog/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/datadog/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/go/references/debug-logs-mcp.md b/skills/posthog/logs/skills/go/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/go/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/java/references/debug-logs-mcp.md b/skills/posthog/logs/skills/java/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/java/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/nextjs/references/debug-logs-mcp.md b/skills/posthog/logs/skills/nextjs/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/nextjs/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/nodejs/references/debug-logs-mcp.md b/skills/posthog/logs/skills/nodejs/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/nodejs/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/other/references/debug-logs-mcp.md b/skills/posthog/logs/skills/other/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/other/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/logs/skills/python/references/debug-logs-mcp.md b/skills/posthog/logs/skills/python/references/debug-logs-mcp.md deleted file mode 100644 index f57bb977..00000000 --- a/skills/posthog/logs/skills/python/references/debug-logs-mcp.md +++ /dev/null @@ -1,50 +0,0 @@ -# Debug Logs with MCP - Docs - -The [PostHog MCP server](/docs/model-context-protocol.md) gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor. - -With MCP, your agents can: - -- **Search and filter logs** – Query by severity level, service name, date range, and free text. -- **Discover log attributes** – List available attributes and their values to build targeted queries. -- **Debug in context** – Investigate production issues without switching tools. -- **Correlate with traces** – Use trace IDs and span IDs from log entries to follow request flows across services. - -This works in any MCP client – Cursor, Windsurf, Claude Code, and others. - -## Example prompts - -Try these prompts with your MCP-enabled agent: - -- `Show me all error logs from the last hour.` -- `What services are logging errors? Search for error logs from the payments service.` -- `Find logs related to trace ID abc123.` -- `What log attributes are available? Show me the values for the service.name attribute.` -- `Show me warning and error logs from the last 24 hours, excluding debug noise.` - -## Logs tools - -The MCP server provides three tools for working with logs: - -| Tool | Description | -| --- | --- | -| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. | -| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). | -| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. | - -A typical workflow: - -1. Call `logs-list-attributes` to discover available filter attributes. -2. Call `logs-list-attribute-values` to find specific values (e.g., which service names exist). -3. Call `logs-query` to search logs with the right filters. - -## Get started - -See the [MCP server documentation](/docs/model-context-protocol.md) for setup instructions. - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better \ No newline at end of file From 98e588b8016555796b3355b3cf0cd749a1b06a58 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:30:14 -0400 Subject: [PATCH 02/13] Update generated plugins from context-mill (mirror test) --- skills/posthog/all/.claude-plugin/plugin.json | 2 +- .../skills/creating-product-tours/SKILL.md | 410 ++++++++++ .../references/COMMANDMENTS.md | 5 + .../skills/error-tracking-android/SKILL.md | 4 +- .../references/COMMANDMENTS.md | 9 + .../references/alerts.md | 16 +- .../references/android.md | 14 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../skills/error-tracking-angular/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 12 + .../references/alerts.md | 16 +- .../references/angular.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-django/SKILL.md | 51 ++ .../references/COMMANDMENTS.md | 20 + .../references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/django.md | 300 +++++++ .../references/fingerprints.md | 63 ++ .../references/monitoring.md | 146 ++++ .../references/python.md | 191 +++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-dotnet/SKILL.md | 46 ++ .../references/COMMANDMENTS.md | 15 + .../references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/dotnet.md | 773 ++++++++++++++++++ .../references/fingerprints.md | 63 ++ .../references/monitoring.md | 146 ++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-elixir/SKILL.md | 46 ++ .../references/COMMANDMENTS.md | 16 + .../references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/elixir.md | 316 +++++++ .../references/fingerprints.md | 63 ++ .../references/monitoring.md | 146 ++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-flask/SKILL.md | 49 ++ .../references/COMMANDMENTS.md | 18 + .../error-tracking-flask/references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/fingerprints.md | 63 ++ .../error-tracking-flask/references/flask.md | 147 ++++ .../references/monitoring.md | 146 ++++ .../error-tracking-flask/references/python.md | 191 +++++ .../references/upload-source-maps.md | 67 ++ .../skills/error-tracking-flutter/SKILL.md | 14 +- .../references/COMMANDMENTS.md | 14 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/flutter.md | 33 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-go/SKILL.md | 15 +- .../references/COMMANDMENTS.md | 15 + .../error-tracking-go/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../skills/error-tracking-go/references/go.md | 18 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-hono/SKILL.md | 8 +- .../references/COMMANDMENTS.md | 8 + .../error-tracking-hono/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../error-tracking-hono/references/hono.md | 12 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-ios/SKILL.md | 50 ++ .../references/COMMANDMENTS.md | 20 + .../error-tracking-ios/references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/fingerprints.md | 63 ++ .../error-tracking-ios/references/ios.md | 264 ++++++ .../references/monitoring.md | 146 ++++ .../references/upload-source-maps.md | 67 ++ .../skills/error-tracking-laravel/SKILL.md | 46 ++ .../references/COMMANDMENTS.md | 15 + .../references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/fingerprints.md | 63 ++ .../references/laravel.md | 176 ++++ .../references/monitoring.md | 146 ++++ .../error-tracking-laravel/references/php.md | 228 ++++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-nextjs/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 17 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/nextjs.md | 26 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-node/SKILL.md | 11 +- .../references/COMMANDMENTS.md | 15 + .../error-tracking-node/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../error-tracking-node/references/node.md | 12 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-nuxt/SKILL.md | 11 +- .../references/COMMANDMENTS.md | 8 + .../error-tracking-nuxt/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/nuxt-3-6.md | 257 ++++++ .../references/nuxt-3-7.md | 187 +++++ .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-php/SKILL.md | 41 + .../references/COMMANDMENTS.md | 11 + .../error-tracking-php/references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/fingerprints.md | 63 ++ .../references/monitoring.md | 146 ++++ .../error-tracking-php/references/php.md | 228 ++++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-python/SKILL.md | 4 +- .../references/COMMANDMENTS.md | 15 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/python.md | 14 +- .../references/upload-source-maps.md | 28 +- .../error-tracking-react-native/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 12 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/react-native.md | 31 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-react/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 16 + .../error-tracking-react/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../error-tracking-react/references/react.md | 24 +- .../references/upload-source-maps.md | 28 +- .../error-tracking-ruby-on-rails/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 23 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/ruby-on-rails.md | 644 ++++++++++++--- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-ruby/SKILL.md | 4 +- .../references/COMMANDMENTS.md | 11 + .../error-tracking-ruby/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../error-tracking-ruby/references/ruby.md | 18 +- .../references/upload-source-maps.md | 28 +- .../all/skills/error-tracking-rust/SKILL.md | 44 + .../references/COMMANDMENTS.md | 14 + .../error-tracking-rust/references/alerts.md | 69 ++ .../references/assigning-issues.md | 103 +++ .../references/fingerprints.md | 63 ++ .../references/monitoring.md | 146 ++++ .../error-tracking-rust/references/rust.md | 199 +++++ .../references/upload-source-maps.md | 67 ++ .../all/skills/error-tracking-svelte/SKILL.md | 8 +- .../references/COMMANDMENTS.md | 11 + .../references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/svelte.md | 14 +- .../references/upload-source-maps.md | 28 +- .../SKILL.md | 409 +++++++++ .../references/COMMANDMENTS.md | 9 + .../references/android.md | 183 +++++ .../references/cli.md | 150 ++++ .../references/upload-source-maps.md | 67 ++ .../SKILL.md | 412 ++++++++++ .../references/COMMANDMENTS.md | 12 + .../references/angular.md | 204 +++++ .../references/cli.md | 150 ++++ .../references/upload-source-maps.md | 67 ++ .../SKILL.md | 417 ++++++++++ .../references/COMMANDMENTS.md | 14 + .../references/android.md | 183 +++++ .../references/cli.md | 150 ++++ .../references/flutter.md | 48 ++ .../references/ios.md | 286 +++++++ .../references/upload-source-maps.md | 67 ++ 200 files changed, 13069 insertions(+), 603 deletions(-) create mode 100644 skills/posthog/all/skills/creating-product-tours/SKILL.md create mode 100644 skills/posthog/all/skills/creating-product-tours/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-android/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-angular/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-django/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/django.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/python.md create mode 100644 skills/posthog/all/skills/error-tracking-django/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/dotnet.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-dotnet/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/elixir.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-elixir/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/flask.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/python.md create mode 100644 skills/posthog/all/skills/error-tracking-flask/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-flutter/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-go/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-hono/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/ios.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-ios/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/laravel.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/php.md create mode 100644 skills/posthog/all/skills/error-tracking-laravel/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-nextjs/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-node/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-nuxt/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-6.md create mode 100644 skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-7.md create mode 100644 skills/posthog/all/skills/error-tracking-php/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/php.md create mode 100644 skills/posthog/all/skills/error-tracking-php/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-python/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-react-native/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-react/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-ruby-on-rails/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-ruby/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/rust.md create mode 100644 skills/posthog/all/skills/error-tracking-rust/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-svelte/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-android/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/android.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-angular/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/angular.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/android.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/flutter.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/ios.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/upload-source-maps.md diff --git a/skills/posthog/all/.claude-plugin/plugin.json b/skills/posthog/all/.claude-plugin/plugin.json index 9d3da86b..005b79d2 100644 --- a/skills/posthog/all/.claude-plugin/plugin.json +++ b/skills/posthog/all/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "posthog-all", "description": "Complete set of all PostHog skills for agents and power users", - "version": "1.9.4", + "version": "dev", "author": { "name": "PostHog" }, diff --git a/skills/posthog/all/skills/creating-product-tours/SKILL.md b/skills/posthog/all/skills/creating-product-tours/SKILL.md new file mode 100644 index 00000000..98514746 --- /dev/null +++ b/skills/posthog/all/skills/creating-product-tours/SKILL.md @@ -0,0 +1,410 @@ +--- +name: creating-product-tours +description: >- + Build an in-app product tour that guides users through a feature inside the + user's own application. Targeting via PostHog feature flags, tracking via + PostHog events, custom reusable UI components. Use when a user asks to create + a product tour, onboarding walkthrough, feature spotlight, or step-by-step + guide in their app. +metadata: + author: PostHog + version: dev +--- + +# Building an in-app product tour with PostHog + +Product tours use PostHog feature flags for targeting (who sees the tour and when) and PostHog events for tracking (completion, drop-off, step funnel). UI components should be custom-built but reusable across multiple tours. + +**Local-dev behavior**: the tour renders locally even when PostHog isn't initialized (no project token, provider not mounted, ad blocker active). The feature flag infrastructure is still scaffolded for production rollout — it just isn't a hard gate in dev. This lets engineers iterate on the tour without needing a PostHog project wired up. See "Local development" below for how the fail-open works and how to opt out. + +## Step 1: gather requirements + +Ask the user these questions before writing any code: + +1. **Goal** — What should the user be able to do after completing this tour? (e.g., "create their first dashboard", "connect a data source") +2. **Steps** — List all steps: what element should it point to, what does it say, how does the user advance? (e.g, "Step 1: Highlight the Create project button and ask the user to click it. Step 2: Highlight the SDK install command and explain how to copy it. Step 3: Highlight the dashboard page and prompt the user to create their first insight.") +3. **Audience** — Who sees it? All users, new users only, users on a specific plan, users who haven't done something yet? +4. **Trigger** — Auto-show on page load, trigger from a button, or chain from another tour? +5. **Show frequency** — Once ever, once per session, or every time the user visits? +6. **Tech stack** — React, Vue, vanilla JS? This determines component shape. + +Do not proceed until you have goal, at least one step, and the tech stack. + +## Step 2: create the feature flag + +Create one feature flag per tour in PostHog. This controls targeting independently of the code. + +Naming convention: `tour--` — e.g. `tour-dashboards-first-chart`, `tour-onboarding-data-source`. + +Use the PostHog MCP tool or the PostHog UI (Flags → New feature flag): + +- **Key**: `tour-` (lowercase, hyphens) +- **Rollout**: set to the target audience using person/group properties or a percentage rollout +- **Enabled state**: Leave the feature flag disabled – the user enables it when ready. +- **Payload** (optional): store the step config as JSON so tours can be updated without deploys: + +```json +{ + "steps": [ + { + "target": "#new-dashboard-btn", + "title": "Create a dashboard", + "body": "Click here to start.", + "placement": "bottom" + }, + { + "target": ".dashboard-name-input", + "title": "Name your dashboard", + "body": "Give it a memorable name.", + "placement": "right" + } + ] +} +``` + +## Step 3: build reusable tour components + +Build these once and reuse them for every tour. Do not create one-off components per tour. If not using react, adapt the strategy to what makes most sense for the platform. + +### Core hook: `useTour` + +```tsx +// hooks/useTour.ts +import { useEffect, useState, useCallback } from "react"; +import posthog from "posthog-js"; + +export interface TourStep { + target: string; // CSS selector for the anchor element + title: string; + body: string; + placement?: "top" | "bottom" | "left" | "right"; +} + +interface UseTourOptions { + flagKey: string; + steps: TourStep[]; // inline step definitions — required so the tour renders without a PostHog payload + storageKey?: string; // localStorage key to remember completion; defaults to `tour-${flagKey}` + // When true, the flag check is enforced even if PostHog isn't initialized + // (the tour won't render locally without PostHog wired up). Default false: + // fail-open in dev so engineers can iterate without a project token. + requireFlag?: boolean; +} + +// posthog-js sets __loaded = true once init() succeeds. We use it to detect +// whether PostHog is actually wired up before treating its flag answer as truth. +function isPosthogReady(): boolean { + return Boolean((posthog as unknown as { __loaded?: boolean }).__loaded); +} + +export function useTour({ + flagKey, + steps: staticSteps, + storageKey, + requireFlag = false, +}: UseTourOptions) { + const key = storageKey ?? `tour-${flagKey}`; + const [active, setActive] = useState(false); + const [stepIndex, setStepIndex] = useState(0); + const [steps, setSteps] = useState(staticSteps); + + useEffect(() => { + if (localStorage.getItem(key) === "done") return; + + const ready = isPosthogReady(); + if (ready) { + // Production path: PostHog is wired, the flag decides. + const result = posthog.getFeatureFlagResult(flagKey); + if (!result?.enabled) return; + const payload = result.payload as { + steps?: TourStep[]; + } | null; + if (payload?.steps) setSteps(payload.steps); + } else if (requireFlag) { + // Strict mode: no PostHog, no tour. + return; + } + // else: fail-open — render with the inline staticSteps so local dev works. + + setActive(true); + posthog.capture("tour started", { tour_id: flagKey }); + }, [flagKey, key, requireFlag]); + + const advance = useCallback(() => { + const next = stepIndex + 1; + posthog.capture("tour step completed", { + tour_id: flagKey, + step: stepIndex, + }); + if (next >= steps.length) { + localStorage.setItem(key, "done"); + setActive(false); + posthog.capture("tour completed", { tour_id: flagKey }); + } else { + setStepIndex(next); + posthog.capture("tour step viewed", { tour_id: flagKey, step: next }); + } + }, [flagKey, key, stepIndex, steps.length]); + + const dismiss = useCallback(() => { + localStorage.setItem(key, "done"); + setActive(false); + posthog.capture("tour dismissed", { tour_id: flagKey, at_step: stepIndex }); + }, [flagKey, key, stepIndex]); + + return { + active, + step: steps[stepIndex] ?? null, + stepIndex, + totalSteps: steps.length, + advance, + dismiss, + }; +} +``` + +### Display component: `TourTooltip` + +```tsx +// components/TourTooltip.tsx +import { useEffect, useRef, useState } from "react"; +import type { TourStep } from "../hooks/useTour"; + +interface TourTooltipProps { + step: TourStep; + stepIndex: number; + totalSteps: number; + onNext: () => void; + onDismiss: () => void; +} + +export function TourTooltip({ + step, + stepIndex, + totalSteps, + onNext, + onDismiss, +}: TourTooltipProps) { + const [position, setPosition] = useState({ top: 0, left: 0 }); + const tooltipRef = useRef(null); + + useEffect(() => { + const target = document.querySelector(step.target); + if (!target || !tooltipRef.current) return; + + const rect = target.getBoundingClientRect(); + const tip = tooltipRef.current.getBoundingClientRect(); + const placement = step.placement ?? "bottom"; + + const GAP = 12; + const pos = { + top: + placement === "bottom" + ? rect.bottom + GAP + window.scrollY + : placement === "top" + ? rect.top - tip.height - GAP + window.scrollY + : rect.top + rect.height / 2 - tip.height / 2 + window.scrollY, + left: + placement === "left" + ? rect.left - tip.width - GAP + window.scrollX + : placement === "right" + ? rect.right + GAP + window.scrollX + : rect.left + rect.width / 2 - tip.width / 2 + window.scrollX, + }; + setPosition(pos); + + // Highlight the target element + target.setAttribute("data-tour-active", "true"); + return () => target.removeAttribute("data-tour-active"); + }, [step]); + + return ( +
+ +

{step.title}

+

{step.body}

+
+ + {stepIndex + 1} / {totalSteps} + + +
+
+ ); +} +``` + +### Minimal CSS (adapt to match the app's design system) + +```css +/* tour.css */ +.tour-tooltip { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + padding: 16px; + min-width: 240px; + max-width: 340px; +} +.tour-tooltip__close { + position: absolute; + top: 8px; + right: 8px; + background: none; + border: none; + cursor: pointer; + color: #6b7280; +} +.tour-tooltip__title { + margin: 0 0 8px; + font-weight: 600; + font-size: 14px; +} +.tour-tooltip__body { + margin: 0 0 12px; + font-size: 13px; + color: #374151; +} +.tour-tooltip__footer { + display: flex; + justify-content: space-between; + align-items: center; +} +.tour-tooltip__progress { + font-size: 12px; + color: #9ca3af; +} +.tour-tooltip__next { + background: #f54e00; + color: #fff; + border: none; + border-radius: 6px; + padding: 6px 14px; + cursor: pointer; + font-size: 13px; +} +/* Highlight ring on the targeted element */ +[data-tour-active="true"] { + outline: 2px solid #f54e00; + outline-offset: 3px; + border-radius: 4px; +} +``` + +### Wiring it together + +```tsx +// In the page or app root where the tour should run: +import { useTour, TourStep } from "../hooks/useTour"; +import { TourTooltip } from "../components/TourTooltip"; + +// Inline steps are the source of truth in code so the tour renders even when +// PostHog isn't wired (local dev). In production, a flag payload can override these. +const DASHBOARDS_FIRST_CHART_STEPS: TourStep[] = [ + { target: "#new-dashboard-btn", title: "Create a dashboard", body: "Click here to start.", placement: "bottom" }, + { target: ".dashboard-name-input", title: "Name your dashboard", body: "Give it a memorable name.", placement: "right" }, +]; + +export function DashboardPage() { + const tour = useTour({ + flagKey: "tour-dashboards-first-chart", + steps: DASHBOARDS_FIRST_CHART_STEPS, + }); + + return ( + <> + {/* ... page content ... */} + {tour.active && tour.step && ( + + )} + + ); +} +``` + +## Local development + +The `useTour` hook is designed to **fail-open when PostHog isn't initialized**. The decision tree on mount: + +| PostHog state | Flag result | Behavior | +| -------------------------------------- | ------------- | ------------------------------------- | +| Initialized (`posthog.__loaded`) | `true` | Show tour. Apply payload steps if present. | +| Initialized | `false` | Hide tour. | +| Not initialized (no key, blocked, etc) | n/a | **Show tour** using inline `staticSteps`. | +| Not initialized + `requireFlag: true` | n/a | Hide tour (strict mode). | + +Why: engineers should be able to build and demo a tour without wiring PostHog. The flag, payload, events, and targeting code are all in place — they just don't *gate* rendering during local dev. In production, where PostHog is initialized, the flag is fully respected. + +To preview the disabled state locally: + +- Set `requireFlag: true` on the hook call, **or** +- Wire PostHog locally and toggle the flag off in the PostHog UI + +Note: `posthog.capture(...)` calls inside the hook are no-ops when PostHog isn't initialized, so they're safe to leave in. + +## Adding a second tour + +Reuse the same hook and component — only the flag key and steps change: + +```tsx +const tour = useTour({ + flagKey: "tour-settings-integrations", + steps: SETTINGS_INTEGRATIONS_STEPS, +}); +``` + +No new components needed. If steps differ enough to need a different layout (e.g., a modal instead of a tooltip), create a second display component (`TourModal`) that accepts the same props shape as `TourTooltip`. + +## Targeting patterns + +| Goal | Flag setup | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| All new users | Roll out 100%, set a person property `onboarding_tour_shown` via `posthog.people.set` after completion to suppress on re-identification | +| Paid plan only | Filter by `plan = 'paid'` person property | +| After a specific action | Trigger `useTour` only after the user completes the prerequisite; the flag controls the audience, the trigger controls timing | +| A/B test the tour | Use a multivariate flag with variants `control` / `tour` and check `posthog.getFeatureFlag(key) === 'tour'` | + +## Events to analyse + +All events emitted by `useTour` above. Query in PostHog: + +- Funnel: `tour started` → `tour step viewed (step 0..N)` → `tour completed` — shows drop-off per step +- `tour dismissed` with `at_step` property — shows where users give up +- Filter all by `tour_id` property to keep tours separate + +## Checklist before shipping + +- [ ] Feature flag created with correct rollout, but not enabled +- [ ] Inline `steps` array passed to `useTour` (so it renders without a PostHog payload) +- [ ] Tour renders locally without PostHog wired up (fail-open verified) +- [ ] Tour renders in production when flag is enabled; hidden when flag is disabled +- [ ] `target` selectors verified in the browser against real DOM nodes +- [ ] `localStorage` key chosen so it doesn't collide with other tours +- [ ] All four events captured in PostHog once wired: `started`, `step viewed`, `completed`, `dismissed` +- [ ] `TourTooltip` / `useTour` live in a shared location, not duplicated per feature +- [ ] User alerted that they should check rollout conditions and enable the flag when they want to see the tour in production diff --git a/skills/posthog/all/skills/creating-product-tours/references/COMMANDMENTS.md b/skills/posthog/all/skills/creating-product-tours/references/COMMANDMENTS.md new file mode 100644 index 00000000..08d1eb78 --- /dev/null +++ b/skills/posthog/all/skills/creating-product-tours/references/COMMANDMENTS.md @@ -0,0 +1,5 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op diff --git a/skills/posthog/all/skills/error-tracking-android/SKILL.md b/skills/posthog/all/skills/error-tracking-android/SKILL.md index 610fc101..8e323a37 100644 --- a/skills/posthog/all/skills/error-tracking-android/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-android/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-android description: PostHog error tracking for Android metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Android @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Android applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version - Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. - Initialize PostHog in the Application class's `onCreate()` method diff --git a/skills/posthog/all/skills/error-tracking-android/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-android/references/COMMANDMENTS.md new file mode 100644 index 00000000..c18de5d4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-android/references/COMMANDMENTS.md @@ -0,0 +1,9 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. diff --git a/skills/posthog/all/skills/error-tracking-android/references/alerts.md b/skills/posthog/all/skills/error-tracking-android/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-android/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-android/references/android.md b/skills/posthog/all/skills/error-tracking-android/references/android.md index 2bb50985..e566f708 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/android.md +++ b/skills/posthog/all/skills/error-tracking-android/references/android.md @@ -1,4 +1,10 @@ -# Android error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Android Error Tracking installation - Docs + +Copy page + +# Android Error Tracking installation - Docs 1. 1 @@ -146,15 +152,15 @@ Required - Great, you're capturing exceptions! If you serve minified bundles, the next step is to upload source maps to generate accurate stack traces. + Great, you're capturing exceptions! The next step is to upload ProGuard/R8 mapping files so PostHog can deobfuscate your stack traces. Let's continue to the next section. [Upload mapping files](/docs/error-tracking/upload-mappings/android.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-android/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-android/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-android/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-android/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-android/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-android/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-android/references/monitoring.md b/skills/posthog/all/skills/error-tracking-android/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-android/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-android/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-android/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-android/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-android/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/SKILL.md b/skills/posthog/all/skills/error-tracking-angular/SKILL.md index 7f4a359e..c5be0543 100644 --- a/skills/posthog/all/skills/error-tracking-angular/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-angular/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-angular description: PostHog error tracking for Angular metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Angular @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Angular applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,7 +32,11 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. - Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. - Always use standalone components over NgModules. - Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-angular/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-angular/references/COMMANDMENTS.md new file mode 100644 index 00000000..3876eeda --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-angular/references/COMMANDMENTS.md @@ -0,0 +1,12 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. +- Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. +- Always use standalone components over NgModules. +- Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-angular/references/alerts.md b/skills/posthog/all/skills/error-tracking-angular/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/references/angular.md b/skills/posthog/all/skills/error-tracking-angular/references/angular.md index a858ec0b..a4ae2aed 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/angular.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/angular.md @@ -1,4 +1,10 @@ -# Angular error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Angular Error Tracking installation - Docs + +Copy page + +# Angular Error Tracking installation - Docs 1. 1 @@ -65,7 +71,7 @@ this.ngZone.runOutsideAngular(() => { posthog.init(environment.posthogKey, { api_host: environment.posthogHost, - defaults: '2026-01-30', + defaults: '2026-05-30', }); }); } @@ -113,7 +119,7 @@ import posthog from 'posthog-js' posthog.init(environment.posthogKey, { api_host: environment.posthogHost, - defaults: '2025-11-30' + defaults: '2026-05-30' }) bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); @@ -276,9 +282,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/angular.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-angular/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-angular/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/references/monitoring.md b/skills/posthog/all/skills/error-tracking-angular/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-angular/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-angular/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-angular/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-angular/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-django/SKILL.md b/skills/posthog/all/skills/error-tracking-django/SKILL.md new file mode 100644 index 00000000..ce9b4c51 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/SKILL.md @@ -0,0 +1,51 @@ +--- +name: error-tracking-django +description: PostHog error tracking for Django +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for Django + +This skill helps you add PostHog error tracking to Django applications. + +## Reference files + +- `references/python.md` - Python error tracking installation - docs +- `references/django.md` - Django - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions +- Initialize PostHog in AppConfig.ready() with api_key and host from environment variables +- The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own +- The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() +- Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/error-tracking-django/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-django/references/COMMANDMENTS.md new file mode 100644 index 00000000..ca143695 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions +- Initialize PostHog in AppConfig.ready() with api_key and host from environment variables +- The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own +- The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() +- Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/error-tracking-django/references/alerts.md b/skills/posthog/all/skills/error-tracking-django/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-django/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/django.md b/skills/posthog/all/skills/error-tracking-django/references/django.md new file mode 100644 index 00000000..e143a17f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/django.md @@ -0,0 +1,300 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Django - Docs + +Copy page + +# Django - Docs + +PostHog makes it easy to get data about traffic and usage of your Django app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more. + +This guide walks you through integrating PostHog into your Django app using the [Python SDK](/docs/libraries/python.md). + +## Beta: integration via LLM + +Install PostHog for Django in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. + +`npx @posthog/wizard` + +[Learn more](/wizard.md) + +Or, to integrate manually, continue with the rest of this guide. + +> These docs cover version `7.x` of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See [supported versions](#supported-versions). + +## Installation + +To start, run `pip install posthog` to install PostHog’s Python SDK. + +Then, configure PostHog in your app config so it's initialized when Django starts: + +your\_app/apps.py + +PostHog AI + +```python +from django.apps import AppConfig +import posthog +class YourAppConfig(AppConfig): + name = 'your_app_name' + def ready(self): + posthog.api_key = '' + posthog.host = 'https://us.i.posthog.com' +``` + +Next, if you haven't done so already, add your `AppConfig` to `INSTALLED_APPS` in `settings.py`: + +settings.py + +PostHog AI + +```python +INSTALLED_APPS = [ + # ... other apps + 'your_app_name.apps.YourAppConfig', +] +``` + +You can find your project token and instance address in [your project settings](https://app.posthog.com/project/settings). + +To capture events from any file, import `posthog` and call the method you need. For example: + +Python + +PostHog AI + +```python +import posthog +from posthog import identify_context +def some_request(request): + with posthog.new_context(): + # Django includes request.user for anonymous visitors too. Only identify + # the context when the visitor is logged in. + if request.user.is_authenticated: + identify_context(str(request.user.pk)) + posthog.capture('event_name') +``` + +Events captured without a context or explicit `distinct_id` are sent as [anonymous events](/docs/data/anonymous-vs-identified-events.md) with an auto-generated `distinct_id`. See the [Python SDK docs](/docs/libraries/python.md#person-profiles-and-properties) for more details. + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` to associate events with the correct user. +> +> In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct `distinct_id`. Typically, you would set a fresh context and identify at the top of each route. +> +> Python +> +> PostHog AI +> +> ```python +> from posthog import new_context, identify_context, capture +> @app.get("/foo") +> def foo(current_user: User = Depends(get_current_user)): +> with new_context(): # Set context at the top of a route +> identify_context(current_user.id) +> capture("foo_viewed") +> return {"status": "ok"} +> ``` +> +> When possible, write a small piece of **middleware** that resolves your authenticated user, wrap a context around the request, and identifies it. Every `capture()` downstream is then attributed *automatically*. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK. + +## Django contexts middleware + +The Python SDK provides a Django middleware that automatically wraps all requests with a [context](/docs/libraries/python.md#contexts). This middleware extracts session and user information from each request and tags all events captured during that request with relevant metadata. + +### Basic setup + +Add the middleware to your Django settings. If your app uses Django authentication, place it after `django.contrib.auth.middleware.AuthenticationMiddleware` so the middleware can use the authenticated Django user as a distinct ID fallback and capture the user's email. + +Python + +PostHog AI + +```python +MIDDLEWARE = [ + # ... other middleware + 'posthog.integrations.django.PosthogContextMiddleware', + # ... other middleware +] +``` + +The middleware uses the globally configured `posthog` client by default, so you don't need to create or pass it a separate client instance. + +The middleware automatically extracts and uses: + +- **Session ID** from the `X-POSTHOG-SESSION-ID` header, if present +- **Distinct ID** from the `X-POSTHOG-DISTINCT-ID` header, if present, falling back to the authenticated Django user's `pk` (Django's primary-key alias, which works with custom user models) +- **User email** from the authenticated Django user's `email` as `email` +- **Current URL** as `$current_url` +- **Request method** as `$request_method` +- **Request path** as `$request_path` +- **Forwarded IP address** from `X-Forwarded-For` as `$ip` +- **User agent** from `User-Agent` as `$user_agent` + +The session and distinct ID headers are sanitized before use. Empty values are ignored, control characters are removed, values are trimmed, and values are capped at 1000 characters. + +All events captured during the request (including exceptions) include these properties and are associated with the extracted session and distinct ID. + +### Login and signup views + +The middleware reads `request.user` once, before your view runs. On a login or signup request the visitor is still anonymous at that point, so the request's context has no distinct ID. Calling `login()` inside the view doesn't change that. Everything captured during that request stays anonymous, including the login event itself. + +Identify the context from inside the request once you know who the user is. Django's auth signals are the natural place: + +Python + +PostHog AI + +```python +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver +from posthog import identify_context +@receiver(user_logged_in) +def identify_posthog_user(sender, request, user, **kwargs): + identify_context(str(user.pk)) +``` + +Every capture later in that request is then attributed to the user who just logged in. Requests made after login don't need this. The middleware sees the authenticated user from the start. + +If you're using [PostHog JavaScript Web](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Django backend hostname so browser requests include the session and distinct ID headers. + +### Exception capture + +By default, the middleware captures exceptions and sends them to PostHog's error tracking using the globally configured `posthog` client. This includes Django view exceptions that Django converts into error responses. + +Disable this by setting: + +Python + +PostHog AI + +```python +# settings.py +POSTHOG_MW_CAPTURE_EXCEPTIONS = False +``` + +### Adding custom tags + +Use `POSTHOG_MW_EXTRA_TAGS` to add custom properties to all requests: + +Python + +PostHog AI + +```python +# settings.py +def add_user_tags(request): + # type: (HttpRequest) -> Dict[str, Any] + tags = {} + if hasattr(request, 'user') and request.user.is_authenticated: + # Use pk instead of id so this works with custom User primary keys. + tags['user_id'] = str(request.user.pk) + tags['email'] = request.user.email + return tags +POSTHOG_MW_EXTRA_TAGS = add_user_tags +``` + +#### Filtering requests + +Skip tracking for certain requests using `POSTHOG_MW_REQUEST_FILTER`: + +Python + +PostHog AI + +```python +# settings.py +def should_track_request(request): + # type: (HttpRequest) -> bool + # Don't track health checks or admin requests + if request.path.startswith('/health') or request.path.startswith('/admin'): + return False + return True +POSTHOG_MW_REQUEST_FILTER = should_track_request +``` + +### Modifying default tags + +Use `POSTHOG_MW_TAG_MAP` to modify or remove default tags: + +Python + +PostHog AI + +```python +# settings.py +def customize_tags(tags): + # type: (Dict[str, Any]) -> Dict[str, Any] + # Remove URL for privacy + tags.pop('$current_url', None) + # Add custom prefix to method + if '$request_method' in tags: + tags['http_method'] = tags.pop('$request_method') + return tags +POSTHOG_MW_TAG_MAP = customize_tags +``` + +### Complete configuration example + +Python + +PostHog AI + +```python +# settings.py +def add_request_context(request): + # type: (HttpRequest) -> Dict[str, Any] + tags = {} + if hasattr(request, 'user') and request.user.is_authenticated: + tags['user_type'] = 'authenticated' + # Use pk instead of id so this works with custom User primary keys. + tags['user_id'] = str(request.user.pk) + else: + tags['user_type'] = 'anonymous' + # Add request info + tags['user_agent'] = request.META.get('HTTP_USER_AGENT', '') + return tags +def filter_tracking(request): + # type: (HttpRequest) -> bool + # Skip internal endpoints + return not request.path.startswith(('/health', '/metrics', '/admin')) +def clean_tags(tags): + # type: (Dict[str, Any]) -> Dict[str, Any] + # Remove sensitive data + tags.pop('user_agent', None) + return tags +POSTHOG_MW_EXTRA_TAGS = add_request_context +POSTHOG_MW_REQUEST_FILTER = filter_tracking +POSTHOG_MW_TAG_MAP = clean_tags +POSTHOG_MW_CAPTURE_EXCEPTIONS = True +``` + +All events captured within the request context automatically include the configured tags and are associated with the session and user identified from the request headers or Django authentication. + +The middleware supports both sync (WSGI) and async (ASGI) Django applications. In async mode, it uses Django's `request.auser()` API when available to avoid synchronous user access. + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Django (such as analytics, feature flags, A/B testing, etc.), have a look at our [Python SDK docs](/docs/libraries/python.md). + +Alternatively, the following tutorials can help you get started: + +- [Setting up Django analytics, feature flags, and more](/tutorials/django-analytics.md) +- [How to set up A/B tests in Django](/tutorials/django-ab-tests.md) + +## Supported versions + +These docs cover version `7.x` of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on `7.x.x` and higher — pin to the 6.x line with `pip install 'posthog<7'`, where `6.9.3` is the final release. + +Everything on this page works the same way on `6.9.3`. Event capture, the context API (`new_context`, `identify_context`, `set_context_session`), and `PosthogContextMiddleware` are identical on `6.9.3` and `7.0.0` — `7.0.0` only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the `X-POSTHOG-DISTINCT-ID` header and falling back to the authenticated user, which behaves the same across both lines. + +Later `7.x` releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and `set_context_device_id`. They also changed the middleware's own captured properties: `7.x` sends the request IP as `$ip`, where `6.9.3` sends it as `$ip_address`, and `7.x` additionally captures `$request_path`, `$raw_user_agent`, and the authenticated user's `email`. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-django/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/monitoring.md b/skills/posthog/all/skills/error-tracking-django/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/python.md b/skills/posthog/all/skills/error-tracking-django/references/python.md new file mode 100644 index 00000000..f442432b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/python.md @@ -0,0 +1,191 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Error Tracking installation - Docs + +Copy page + +# Python Error Tracking installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog Python library using pip: + + Terminal + + PostHog AI + + ```bash + pip install posthog + ``` + +2. 2 + + ## Initialize PostHog + + Required + + Initialize the PostHog client with your project token and host from your project settings: + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog( + project_api_key='', + host='https://us.i.posthog.com' + ) + ``` + + **Django integration** + + If you're using Django, check out our [Django integration](/docs/libraries/django.md) for automatic request tracking. + +3. 3 + + ## Send events + + Recommended + + Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration: + + Capture custom events by calling the `capture` method with an event name and properties: + + Python + + PostHog AI + + ```python + import posthog + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) + ``` + +4. ## Verify PostHog is initialized + + Recommended + + Before proceeding, enable debug and call `posthog.capture('test_event')` to make sure you can capture events. + +5. 4 + + ## Setting up exception autocapture + + Recommended + + Exception autocapture can be enabled during initialization of the PostHog client to automatically capture any unhandled exceptions thrown by your Python application. It works by setting Python's built-in exception hooks, such as `sys.excepthook` and `threading.excepthook`. + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog("", enable_exception_autocapture=True, ...) + ``` + + We recommend setting up and using [contexts](/docs/libraries/python.md#contexts) so that exceptions automatically include distinct IDs, session IDs, and other properties you can set up with tags. + + You can also enable [code variables capture](/docs/error-tracking/code-variables/python.md) to automatically capture the state of local variables when exceptions occur, giving you a debugger-like view of your application. + +6. 5 + + ## Manually capturing exceptions + + Optional + + For exceptions handled by your application that you would still like sent to PostHog, you can manually call the capture method: + + Python + + PostHog AI + + ```python + posthog.capture_exception(e, distinct_id="user_distinct_id", properties=additional_properties) + ``` + + You can find a full example of all of this in our [Python (and Flask) error tracking tutorial](/tutorials/python-error-tracking.md). + +7. 6 + + ## Framework-specific exception capture + + Optional + + Python frameworks often have built-in error handlers. This means PostHog's default exception autocapture won't work and we need to manually capture errors instead. The exact process depends on the framework: + + ## Django + + The Python SDK provides a Django middleware that automatically wraps all requests with a [context](/docs/libraries/python.md#contexts). Add the middleware to your Django settings: + + Python + + PostHog AI + + ```python + MIDDLEWARE = [ + # ... other middleware + 'posthog.integrations.django.PosthogContextMiddleware', + # ... other middleware + ] + ``` + + By default, the middleware captures exceptions and sends them to PostHog. Disable with `POSTHOG_MW_CAPTURE_EXCEPTIONS = False`. Use `POSTHOG_MW_EXTRA_TAGS`, `POSTHOG_MW_REQUEST_FILTER`, and `POSTHOG_MW_TAG_MAP` to customize. See the [Django integration docs](/docs/libraries/django.md) for full configuration. + + ## Flask + + Python + + PostHog AI + + ```python + from flask import Flask, jsonify + from posthog import Posthog + posthog = Posthog('', host='https://us.i.posthog.com') + @app.errorhandler(Exception) + def handle_exception(e): + event_id = posthog.capture_exception(e) + response = jsonify({'message': str(e), 'error_id': event_id}) + response.status_code = 500 + return response + ``` + + ## FastAPI + + Python + + PostHog AI + + ```python + from fastapi.responses import JSONResponse + from posthog import Posthog + posthog = Posthog('', host='https://us.i.posthog.com') + @app.exception_handler(Exception) + async def http_exception_handler(request, exc): + posthog.capture_exception(exc) + return JSONResponse(status_code=500, content={'message': str(exc)}) + ``` + +8. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-django/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-django/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-django/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/SKILL.md b/skills/posthog/all/skills/error-tracking-dotnet/SKILL.md new file mode 100644 index 00000000..debadb8b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/SKILL.md @@ -0,0 +1,46 @@ +--- +name: error-tracking-dotnet +description: PostHog error tracking for .NET / ASP.NET Core +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for .NET / ASP.NET Core + +This skill helps you add PostHog error tracking to .NET / ASP.NET Core applications. + +## Reference files + +- `references/dotnet.md` - .net error tracking installation - docs +- `references/dotnet.md` - .net - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet +- In ASP.NET Core apps, prefer `builder.AddPostHog()` from `PostHog.AspNetCore` and inject `IPostHogClient` from dependency injection instead of manually constructing clients in controllers +- Configure ASP.NET Core apps with the `PostHog` configuration section or environment variable fallbacks such as `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` +- Add product analytics captures at route, controller, or handler boundaries where meaningful user actions occur; do not track every low-level method call +- Capture request exceptions in middleware with `CaptureException` and then rethrow so existing ASP.NET Core error handling still runs +- For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-dotnet/references/COMMANDMENTS.md new file mode 100644 index 00000000..b6f1d737 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet +- In ASP.NET Core apps, prefer `builder.AddPostHog()` from `PostHog.AspNetCore` and inject `IPostHogClient` from dependency injection instead of manually constructing clients in controllers +- Configure ASP.NET Core apps with the `PostHog` configuration section or environment variable fallbacks such as `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` +- Add product analytics captures at route, controller, or handler boundaries where meaningful user actions occur; do not track every low-level method call +- Capture request exceptions in middleware with `CaptureException` and then rethrow so existing ASP.NET Core error handling still runs +- For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/alerts.md b/skills/posthog/all/skills/error-tracking-dotnet/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-dotnet/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/dotnet.md b/skills/posthog/all/skills/error-tracking-dotnet/references/dotnet.md new file mode 100644 index 00000000..23566f00 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/dotnet.md @@ -0,0 +1,773 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# .NET - Docs + +Copy page + +# .NET - Docs + +This is an optional library you can install if you're working with .NET Core. It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance. + +## Installation + +The `PostHog` package supports any .NET platform that targets .NET Standard 2.1 or .NET 8+, including MAUI, Blazor, and console applications. The `PostHog.AspNetCore` package provides additional conveniences for ASP.NET Core applications such as streamlined registration, request-scoped caching, and integration with [.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference). + +> **Note:** We actively test with ASP.NET Core. Other platforms should work but haven't been specifically tested. If you encounter issues, please [report them on GitHub](https://github.com/PostHog/posthog-dotnet/issues). + +> **Not supported:** Classic UWP (requires .NET Standard 2.0 only). Microsoft has [deprecated UWP](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/migrate-to-windows-app-sdk-ovw) in favor of the Windows App SDK. For Unity projects, see our dedicated [Unity SDK](/docs/libraries/unity.md). + +Terminal + +PostHog AI + +```bash +dotnet add package PostHog.AspNetCore +``` + +In your `Program.cs` (or `Startup.cs` for ASP.NET Core 2.x) file, add the following code: + +C# + +PostHog AI + +```csharp +using PostHog; +var builder = WebApplication.CreateBuilder(args); +// Add PostHog to the dependency injection container as a singleton. +builder.AddPostHog(); +``` + +Make sure to configure PostHog with your project token, instance address, and optional personal API key. For example, in `appsettings.json`: + +JSON + +PostHog AI + +```json +{ + "PostHog": { + "ProjectToken": "", + "HostUrl": "https://us.i.posthog.com" + } +} +``` + +> **Note:** If the host is not specified, the default host `https://us.i.posthog.com` is used. + +Use a secrets manager to store your personal API key. For example, when developing locally you can use the `UserSecrets` feature of the `dotnet` CLI: + +Terminal + +PostHog AI + +```bash +dotnet user-secrets init +dotnet user-secrets set "PostHog:PersonalApiKey" "phx_..." +``` + +You can find your project token and instance address in the [project settings](https://app.posthog.com/project/settings) page in PostHog. + +## Working with .NET Feature Management + +`PostHog.AspNetCore` supports [.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference). This enables you to use the tag helper and the `FeatureGateAttribute` in your ASP.NET Core applications to gate access to certain features using PostHog feature flags. + +To use feature flags with the .NET Feature Management library, you'll need to implement the `IPostHogFeatureFlagContextProvider` interface. The quickest way to do that is to inherit from the `PostHogFeatureFlagContextProvider` class and override the `GetDistinctId` and `GetFeatureFlagOptionsAsync` methods. + +C# + +PostHog AI + +```csharp +public class MyFeatureFlagContextProvider(IHttpContextAccessor httpContextAccessor) + : PostHogFeatureFlagContextProvider +{ + protected override string? GetDistinctId() + => httpContextAccessor.HttpContext?.User.Identity?.Name; + protected override ValueTask GetFeatureFlagOptionsAsync() + { + // In a real app, you might get this information from a + // database or other source for the current user. + return ValueTask.FromResult( + new FeatureFlagOptions + { + PersonProperties = new Dictionary + { + ["email"] = "some-test@example.com" + }, + OnlyEvaluateLocally = true + }); + } +} +``` + +Then, register your implementation in `Program.cs` (or `Startup.cs`): + +C# + +PostHog AI + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.AddPostHog(options => { + options.UseFeatureManagement(); +}); +``` + +With this in place, you can now use `feature` tag helpers in your Razor views: + +HTML + +PostHog AI + +```html + +

This is the new feature!

+
+ +

Sorry, no awesome new feature for you.

+
+``` + +Multivariate feature flags are also supported: + +HTML + +PostHog AI + +```html + +

This is the new feature variant A!

+
+ +

This is the new feature variant B!

+
+``` + +You can also use the `FeatureGateAttribute` to gate access to controllers or actions: + +C# + +PostHog AI + +```csharp +[FeatureGate("awesome-new-feature")] +public class NewFeatureController : Controller +{ + public IActionResult Index() + { + return View(); + } +} +``` + +## Using the core package without ASP.NET Core + +If you're not using ASP.NET Core (for example, in a console application, MAUI app, or Blazor WebAssembly), install the `PostHog` package instead of `PostHog.AspNetCore`. This package has no ASP.NET Core dependencies and can be used in any .NET project targeting .NET Standard 2.1 or .NET 8+. + +Terminal + +PostHog AI + +```bash +dotnet add package PostHog +``` + +The `PostHogClient` class must be implemented as a singleton in your project. For `PostHog.AspNetCore`, this is handled by the `builder.AddPostHog();` method. For the `PostHog` package, you can do the following if you're using dependency injection: + +C# + +PostHog AI + +```csharp +builder.Services.AddPostHog(); +``` + +If you're not using a `builder` (such as in a console application), you can do the following: + +C# + +PostHog AI + +```csharp +using PostHog; +var services = new ServiceCollection(); +services.AddPostHog(); +var serviceProvider = services.BuildServiceProvider(); +var posthog = serviceProvider.GetRequiredService(); +``` + +The `AddPostHog` methods accept an optional `Action` parameter that you can use to configure the client. + +If you're not using dependency injection, you can create a static instance of the `PostHogClient` class and use that everywhere in your project: + +C# + +PostHog AI + +```csharp +using PostHog; +public static readonly PostHogClient PostHog = new(new PostHogOptions { + ProjectToken = "", + HostUrl = new Uri("https://us.i.posthog.com"), + PersonalApiKey = Environment.GetEnvironmentVariable( + "PostHog__PersonalApiKey") +}); +``` + +## Debug mode + +If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. + +To see detailed logging, set the log level to `Debug` or `Trace` in `appsettings.json`: + +JSON + +PostHog AI + +```json +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "PostHog": "Trace" + } + }, + ... +} +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +## Capturing events + +You can send custom events using `capture`: + +C# + +PostHog AI + +```csharp +posthog.Capture("distinct_id_of_the_user", "user_signed_up"); +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_the_user", + "user_signed_up", + properties: new() { + ["login_type"] = "email", + ["is_free_trial"] = "true" + } +); +``` + +### Sending page views + +If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send `$pageview` events from your backend like so: + +C# + +PostHog AI + +```csharp +using PostHog; +using Microsoft.AspNetCore.Http.Extensions; +posthog.CapturePageView( + "distinct_id_of_the_user", + HttpContext.Request.GetDisplayUrl()); +``` + +## Request context + +For ASP.NET Core apps using `PostHog.AspNetCore`, add request context middleware before routes that call PostHog. This reads incoming PostHog tracing headers and attaches request metadata to captures, exceptions, and feature flag evaluation inside the request. + +Program.cs + +PostHog AI + +```csharp +using PostHog; +using PostHog.AspNetCore; +var builder = WebApplication.CreateBuilder(args); +builder.AddPostHog(); +var app = builder.Build(); +app.UsePostHogRequestContext(); +``` + +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your ASP.NET Core backend hostname so browser requests include the session and distinct ID headers. + +The middleware reads `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` as request-scoped analytics context. It also adds request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip`. Explicit distinct IDs and event properties always override request context. + +Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side decisions, pass an authenticated distinct ID explicitly. You can ignore tracing headers while still collecting request metadata: + +C# + +PostHog AI + +```csharp +app.UsePostHogRequestContext(options => +{ + options.UseTracingHeaders = false; +}); +``` + +Request-context overloads like `posthog.Capture("checkout started")` and `posthog.EvaluateFlagsAsync()` use the current request distinct ID when one is available. + +## Error tracking + +You can manually capture exceptions using `CaptureException`. This sends a `$exception` event with stack frames, inner exceptions, aggregate exceptions, source context when available, and .NET runtime metadata. + +File names, line numbers, and source context depend on debug information already available from the captured .NET stack trace. PostHog doesn't support uploading .NET PDB files yet, so production builds without runtime-accessible debug information may show less detailed stack frames. + +C# + +PostHog AI + +```csharp +try +{ + ProcessOrder(orderId); +} +catch (Exception exception) +{ + posthog.CaptureException(exception, "user_distinct_id"); +} +``` + +Add custom properties to include request, tenant, or domain context: + +C# + +PostHog AI + +```csharp +posthog.CaptureException( + exception, + "user_distinct_id", + new Dictionary + { + ["order_id"] = orderId, + ["environment"] = "production", + } +); +``` + +For the full setup guide, see the [.NET error tracking installation docs](/docs/error-tracking/installation/dotnet.md). + +Automatic exception capture is not available in the .NET SDK yet. + +## Logs + +[PostHog Logs](/docs/logs.md) doesn't use this SDK. Logs are ingested over OpenTelemetry, so you attach an OTLP exporter to the standard `ILogger` pipeline instead — see the [.NET logs installation guide](/docs/logs/installation/dotnet.md). + +## Person profiles and properties + +The .NET SDK captures identified events by default. These create [person profiles](/docs/data/persons.md). To set [person properties](/docs/data/user-properties.md) in these profiles, include them when capturing an event: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + personPropertiesToSet: new() { ["name"] = "Max Hedgehog" }, + personPropertiesToSetOnce: new() { ["initial_url"] = "/blog" } +); +``` + +For more details on the difference between `$set` and `$set_once`, see our [person properties docs](/docs/data/user-properties.md#what-is-the-difference-between-set-and-set_once). + +To capture [anonymous events](/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's `$process_person_profile` property to `false`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + properties: new() { + ["$process_person_profile"] = false + } +) +``` + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +C# + +PostHog AI + +```csharp +await posthog.AliasAsync("current_distinct_id", "new_distinct_id"); +``` + +We strongly recommend reading our docs on [alias](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Group analytics + +Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the [group analytics](/docs/product-analytics/group-analytics.md) guide for more information. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on our [pricing page](/pricing.md). + +To capture an event and associate it with a group, add the `groups` argument to your `Capture` call: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "user_distinct_id", + "some_event", + groups: [new Group("company", "company_id_in_your_db")]); +``` + +Update properties on a group, use the `GroupIdentifyAsync` method: + +C# + +PostHog AI + +```csharp +await posthog.GroupIdentifyAsync( + type: "company", + key: "company_id_in_your_db", + name: "Awesome Inc.", + properties: new() + { + ["employees"] = 11 + } +); +``` + +The `name` is a special property which is used in the PostHog UI for the name of the group. If you don't specify a `name` property, the group ID will be used instead. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Evaluation contexts + +Configure evaluation contexts so this SDK only evaluates flags intended for the matching application, platform, or product area. For ASP.NET Core apps using `PostHog.AspNetCore`, add them to the `PostHog` configuration section: + +JSON + +PostHog AI + +```json +{ + "PostHog": { + "ProjectToken": "", + "HostUrl": "https://us.i.posthog.com", + "EvaluationContexts": ["main-app", "api", "backend"] + } +} +``` + +For code-based configuration, set `EvaluationContexts` on `PostHogOptions`: + +C# + +PostHog AI + +```csharp +var posthog = new PostHogClient(new PostHogOptions +{ + ProjectToken = "", + HostUrl = new Uri("https://us.i.posthog.com"), + EvaluationContexts = ["main-app", "api", "backend"], +}); +``` + +Remote `/flags` requests from `EvaluateFlagsAsync()` include `evaluation_contexts` when configured. + +For more details, see the [evaluation contexts guide](/docs/feature-flags/evaluation-contexts.md). + +### Local evaluation + +Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. + +It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls. + +For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation.md). + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("user_distinct_id"); +var variant = flags.GetFlag("experiment-feature-flag-key")?.VariantKey; +if (variant == "variant-name") +{ + // Do something +} +``` + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## AI observability + +`PostHog.AI` adds [AI observability](/docs/ai-observability.md) for .NET applications using OpenAI or Azure OpenAI. It is currently pre-release, so expect breaking changes before a stable release. + +For installation instructions, see the [OpenAI guide for .NET](/docs/ai-observability/installation/openai.md#net-support) or the [Azure OpenAI guide for .NET](/docs/ai-observability/installation/azure-openai.md#net-support). + +## GeoIP properties + +The `posthog-dotnet` library disregards the server IP, does not add the GeoIP properties, and does not use the values for feature flag evaluations. + +## Serverless environments (Azure Functions/Render/Lambda/...) + +By default, the library buffers events before sending them to the `/batch` endpoint for better performance. This can lead to lost events in serverless environments if the .NET process is terminated by the platform before the buffer is fully flushed. + +To avoid this, call `await posthog.FlushAsync()` after processing every request by adding it as a middleware to your server. This allows `posthog.Capture()` to remain asynchronous for better performance. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-dotnet/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/monitoring.md b/skills/posthog/all/skills/error-tracking-dotnet/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-dotnet/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-dotnet/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-dotnet/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/SKILL.md b/skills/posthog/all/skills/error-tracking-elixir/SKILL.md new file mode 100644 index 00000000..93ab117b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/SKILL.md @@ -0,0 +1,46 @@ +--- +name: error-tracking-elixir +description: PostHog error tracking for Elixir +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for Elixir + +This skill helps you add PostHog error tracking to Elixir applications. + +## Reference files + +- `references/elixir.md` - Elixir error tracking installation - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get` +- Configure PostHog in application config using `api_host`, `api_key`, and `in_app_otp_apps`; read secrets from environment or runtime config, never hardcode them +- In tests, set `test_mode` to true so events are dropped instead of sent to PostHog +- For Phoenix or Plug apps, add `PostHog.Integrations.Plug` before the router so request context is attached to captured events and errors +- Server-side captures must include a stable `distinct_id` matching frontend identify calls, or set it once per process/request with `PostHog.set_context/1` +- Remember `PostHog.set_context/1` uses Logger metadata and is process-scoped; set context in the request, job, or Task process that captures the event +- For new feature flag code, prefer `PostHog.FeatureFlags.evaluate_flags/1` once per user/request, then read values from `PostHog.FeatureFlags.Evaluations` +- To attribute captures to feature flags, call `PostHog.FeatureFlags.set_in_context/1` with the evaluated snapshot, optionally filtered with `only_accessed/1` or `only/2` +- Avoid deprecated feature flag helpers such as `check/2`, `check!/2`, `get_feature_flag_result/2`, and `get_feature_flag_result!/2` in new code +- Error tracking is enabled by default through Logger; set `in_app_otp_apps`, `capture_level`, and `metadata` to improve error grouping and context +- For source context in releases, enable source code context and run `mix posthog.package_source_code` before `mix release` diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-elixir/references/COMMANDMENTS.md new file mode 100644 index 00000000..69522fb4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/COMMANDMENTS.md @@ -0,0 +1,16 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get` +- Configure PostHog in application config using `api_host`, `api_key`, and `in_app_otp_apps`; read secrets from environment or runtime config, never hardcode them +- In tests, set `test_mode` to true so events are dropped instead of sent to PostHog +- For Phoenix or Plug apps, add `PostHog.Integrations.Plug` before the router so request context is attached to captured events and errors +- Server-side captures must include a stable `distinct_id` matching frontend identify calls, or set it once per process/request with `PostHog.set_context/1` +- Remember `PostHog.set_context/1` uses Logger metadata and is process-scoped; set context in the request, job, or Task process that captures the event +- For new feature flag code, prefer `PostHog.FeatureFlags.evaluate_flags/1` once per user/request, then read values from `PostHog.FeatureFlags.Evaluations` +- To attribute captures to feature flags, call `PostHog.FeatureFlags.set_in_context/1` with the evaluated snapshot, optionally filtered with `only_accessed/1` or `only/2` +- Avoid deprecated feature flag helpers such as `check/2`, `check!/2`, `get_feature_flag_result/2`, and `get_feature_flag_result!/2` in new code +- Error tracking is enabled by default through Logger; set `in_app_otp_apps`, `capture_level`, and `metadata` to improve error grouping and context +- For source context in releases, enable source code context and run `mix posthog.package_source_code` before `mix release` diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/alerts.md b/skills/posthog/all/skills/error-tracking-elixir/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-elixir/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/elixir.md b/skills/posthog/all/skills/error-tracking-elixir/references/elixir.md new file mode 100644 index 00000000..94f948e0 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/elixir.md @@ -0,0 +1,316 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Elixir Error Tracking installation - Docs + +Copy page + +# Elixir Error Tracking installation - Docs + +1. 1 + + ## Install the Elixir SDK + + Required + + Add the [PostHog Elixir SDK](/docs/libraries/elixir.md) to your list of dependencies in `mix.exs`: + + Elixir + + PostHog AI + + ```elixir + def deps do + [ + {:posthog, "~> 2.5"} + ] + end + ``` + + Then run: + + Terminal + + PostHog AI + + ```bash + mix deps.get + ``` + + **Source code context** + + The Elixir SDK supports displaying the surrounding lines of source code in the Error Tracking UI. Since Elixir is a compiled language, source files must be packaged at build time. See the [source context step](#enable-source-code-context-optional) below for setup instructions. + +2. 2 + + ## Configure PostHog + + Required + + Add your project token and host to your config: + + config/config.exs + + PostHog AI + + ```elixir + config :posthog, + api_host: "https://us.i.posthog.com", + api_key: "" + ``` + + To get the most out of Error Tracking, set `in_app_otp_apps` to your application name. This marks stack trace frames from your code as "in-app", making it easier to identify relevant frames in the PostHog UI: + + config/config.exs + + PostHog AI + + ```elixir + config :posthog, + api_host: "https://us.i.posthog.com", + api_key: "", + in_app_otp_apps: [:my_app] + ``` + +3. 3 + + ## Errors are captured automatically + + Required + + Error Tracking is **enabled by default**. The SDK hooks into Elixir's built-in [`Logger`](https://hexdocs.pm/logger/Logger.html) handler system, so it automatically captures: + + - **Unhandled exceptions** – crashes in GenServers, Tasks, and other OTP processes + - **Logger.error calls** – any `Logger.error/1` message at or above the configured level + + No additional code is needed. Any crash or error log in your application is sent to PostHog as a `$exception` event with full stack traces. + + **What gets captured** + + The handler captures log messages based on two rules: + + 1. **Crash reasons are always captured** – any log with a `crash_reason` metadata (e.g., GenServer/Task crashes) is captured regardless of log level. + 2. **Log level filtering** – other messages at or above the configured `capture_level` (default: `:error`) are captured. + +4. 4 + + ## Add Phoenix/Plug integration (recommended) + + Recommended + + If you're using Phoenix or Plug, add the `PostHog.Integrations.Plug` middleware to automatically attach HTTP context (URL, host, path, IP) to error events. + + **For Phoenix**, add it to your `endpoint.ex` before the router: + + lib/my\_app\_web/endpoint.ex + + PostHog AI + + ```elixir + plug PostHog.Integrations.Plug + plug MyAppWeb.Router + ``` + + **For Plug apps**, add it to your router: + + Elixir + + PostHog AI + + ```elixir + defmodule MyRouter do + use Plug.Router + plug PostHog.Integrations.Plug + plug :match + plug :dispatch + # ... routes + end + ``` + + This automatically includes `$current_url`, `$host`, `$pathname`, and `$ip` on every error event that occurs during request processing. It also reads `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` tracing headers, so errors can link back to frontend users and sessions when your client SDK sends those headers. + + If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Phoenix or Plug backend hostname. For more details, see the [Elixir request context docs](/docs/libraries/elixir.md#request-context). + +5. 5 + + ## Identify users on errors (recommended) + + Recommended + + By default, errors are attributed to `"unknown"`. To associate errors with specific users, set a context with a `distinct_id` early in your request lifecycle – for example, in a Plug pipeline after authentication: + + Elixir + + PostHog AI + + ```elixir + PostHog.set_context(%{distinct_id: current_user.id}) + ``` + + This is process-scoped, so any error that occurs in the same process (i.e., the same request) will include the user's distinct ID. + + For Phoenix apps, a common pattern is to add this in a plug or controller action: + + lib/my\_app\_web/plugs/set\_posthog\_context.ex + + PostHog AI + + ```elixir + defmodule MyAppWeb.Plugs.SetPostHogContext do + import Plug.Conn + def init(opts), do: opts + def call(conn, _opts) do + if user = conn.assigns[:current_user] do + PostHog.set_context(%{distinct_id: user.id}) + end + conn + end + end + ``` + + Then add it to your router pipeline: + + Elixir + + PostHog AI + + ```elixir + pipeline :browser do + # ... other plugs + plug MyAppWeb.Plugs.SetPostHogContext + end + ``` + +6. 6 + + ## Configure error tracking options (optional) + + Optional + + The SDK supports several configuration options for Error Tracking: + + config/config.exs + + PostHog AI + + ```elixir + config :posthog, + api_host: "https://us.i.posthog.com", + api_key: "", + # Mark your app's stacktrace frames as "in_app" + in_app_otp_apps: [:my_app], + # Minimum log level to capture (default: :error) + # Set to :warning to also capture warnings, or nil to only capture crashes + capture_level: :error, + # Logger metadata keys to include in error events (default: []) + # Set to :all to include all metadata + metadata: [:request_id, :user_id] + ``` + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | in_app_otp_apps | list of atoms | [] | OTP app names whose stacktrace frames are marked as "in_app" in the UI. | + | capture_level | log level or nil | :error | Minimum log level to capture. Crashes with crash_reason are always captured. Set to nil to only capture crashes. | + | metadata | list of atoms or :all | [] | Logger metadata keys to include as event properties. | + | enable_error_tracking | boolean | true | Set to false to disable automatic Error Tracking entirely. | + | global_properties | map | %{} | Properties added to all captured events (not just errors). | + +7. 7 + + ## Enable source code context (optional) + + Optional + + Since Elixir is a compiled language, source files aren't available at runtime by default. To display the surrounding lines of code in PostHog's Error Tracking UI, you need to package your source code at build time. + + **Step 1:** Enable source context in your config: + + config/config.exs + + PostHog AI + + ```elixir + config :posthog, + api_host: "https://us.i.posthog.com", + api_key: "", + enable_source_code_context: true, + root_source_code_paths: [File.cwd!()], + context_lines: 5 + ``` + + **Step 2:** Package source code before building your release: + + Terminal + + PostHog AI + + ```bash + mix posthog.package_source_code + mix release + ``` + + This reads all `.ex` files from your project, compresses them into `priv/posthog_source.map`, and bundles them with your release. When an error occurs, the SDK matches stack trace frames to the packaged source and includes `pre_context`, `context_line`, and `post_context` in each frame. + + **Development mode** + + In development, if `root_source_code_paths` is set and source files are accessible on disk, the SDK reads them directly at startup – no packaging step needed. + + ### Configuration options + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | enable_source_code_context | boolean | false | Enable source code context in stack frames. | + | root_source_code_paths | list of strings | [] | Root paths to scan for source files. | + | source_code_path_pattern | string | "**/*.ex" | Glob pattern for files to include. | + | source_code_exclude_patterns | list of regexes | [~r"^_build/", ~r"^priv/", ~r"^test/"] | Patterns to exclude. | + | context_lines | integer | 5 | Number of lines to include before and after the error line. | + | source_code_map_path | string | nil | Custom path to a packaged source map file. | + + ### Mix task options + + Terminal + + PostHog AI + + ```bash + # Custom output path + mix posthog.package_source_code --output path/to/output.map + # Custom root paths (overrides config) + mix posthog.package_source_code --root-path /app/lib --root-path /app/src + ``` + +8. ## Verify error tracking + + Recommended + + Trigger a test exception to confirm errors are being sent to PostHog. You should see them appear in the [Error Tracking](https://app.posthog.com/error_tracking) tab. + + Elixir + + PostHog AI + + ```elixir + # In an IEx session or a test route + require Logger + Logger.error("Test error from Elixir") + ``` + + Or raise an exception in a controller or GenServer to test crash capture: + + Elixir + + PostHog AI + + ```elixir + # In a Phoenix controller + def test_error(conn, _params) do + raise "Test exception from Phoenix" + end + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-elixir/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/monitoring.md b/skills/posthog/all/skills/error-tracking-elixir/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-elixir/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-elixir/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-elixir/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/SKILL.md b/skills/posthog/all/skills/error-tracking-flask/SKILL.md new file mode 100644 index 00000000..fd91cdaf --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/SKILL.md @@ -0,0 +1,49 @@ +--- +name: error-tracking-flask +description: PostHog error tracking for Flask +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for Flask + +This skill helps you add PostHog error tracking to Flask applications. + +## Reference files + +- `references/python.md` - Python error tracking installation - docs +- `references/flask.md` - Flask - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) +- Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers +- Blueprint registration happens AFTER PostHog initialization in create_app() +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/error-tracking-flask/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-flask/references/COMMANDMENTS.md new file mode 100644 index 00000000..0beac1a3 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/COMMANDMENTS.md @@ -0,0 +1,18 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) +- Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers +- Blueprint registration happens AFTER PostHog initialization in create_app() +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/error-tracking-flask/references/alerts.md b/skills/posthog/all/skills/error-tracking-flask/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-flask/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-flask/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/flask.md b/skills/posthog/all/skills/error-tracking-flask/references/flask.md new file mode 100644 index 00000000..560fa82f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/flask.md @@ -0,0 +1,147 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Flask - Docs + +Copy page + +# Flask - Docs + +PostHog makes it easy to get data about traffic and usage of your Flask app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more. + +This guide walks you through integrating PostHog into your Flask app using the [Python SDK](/docs/libraries/python.md). + +> These docs cover version `7.x` of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See [supported versions](#supported-versions). + +## Installation + +To start, run `pip install posthog` to install PostHog’s Python SDK. + +Then, initialize PostHog where you'd like to use it. For example, here's how to capture an event in a simple route: + +app.py + +PostHog AI + +```python +from flask import Flask +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog( + '', + host='https://us.i.posthog.com', +) +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + posthog.capture( + 'dashboard_api_called', + distinct_id='distinct_id_of_your_user', + ) + return '', 204 +``` + +You can find your project token and instance address in [your project settings](https://app.posthog.com/project/settings). + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` to associate events with the correct user. +> +> In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct `distinct_id`. Typically, you would set a fresh context and identify at the top of each route. +> +> Python +> +> PostHog AI +> +> ```python +> from posthog import new_context, identify_context, capture +> @app.get("/foo") +> def foo(current_user: User = Depends(get_current_user)): +> with new_context(): # Set context at the top of a route +> identify_context(current_user.id) +> capture("foo_viewed") +> return {"status": "ok"} +> ``` +> +> When possible, write a small piece of **middleware** that resolves your authenticated user, wrap a context around the request, and identifies it. Every `capture()` downstream is then attributed *automatically*. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK. + +## Request contexts + +Use [contexts](/docs/libraries/python.md#contexts) to share identity, session IDs, and tags across multiple captures during a request. + +If you're using [PostHog JavaScript Web](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Flask backend hostname so browser requests include the session and distinct ID headers. + +Then read the incoming headers in your Flask request handler. Tracing headers are client-controlled analytics context, not authentication or authorization, so prefer your authenticated user ID when one is available: + +Python + +PostHog AI + +```python +from flask import request, session +from posthog import identify_context, set_context_session, tag +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + with posthog.new_context(fresh=True): + distinct_id = session.get('user_id') or request.headers.get('X-POSTHOG-DISTINCT-ID') + if distinct_id: + identify_context(str(distinct_id)) + session_id = request.headers.get('X-POSTHOG-SESSION-ID') + if session_id: + set_context_session(session_id) + tag('$current_url', request.url) + tag('$request_method', request.method) + tag('$request_path', request.path) + posthog.capture('dashboard_api_called') + return '', 204 +``` + +Events captured without a context or explicit `distinct_id` are sent as [anonymous events](/docs/data/anonymous-vs-identified-events.md) with an auto-generated `distinct_id`. See the [Python SDK docs](/docs/libraries/python.md#person-profiles-and-properties) for more details. + +## Error tracking + +Flask has built-in error handlers. This means PostHog’s default exception autocapture won’t work and we need to manually capture errors instead using `capture_exception()`: + +Python + +PostHog AI + +```python +from flask import Flask, jsonify +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog('', host='https://us.i.posthog.com') +@app.errorhandler(Exception) +def handle_exception(e): + # Capture methods, including capture_exception, return the UUID of the captured event, + # which you can use to find specific errors users encountered + event_id = posthog.capture_exception(e) + # You can show the event ID to your user, and ask them to include it in bug reports + response = jsonify({'message': str(e), 'error_id': event_id}) + response.status_code = 500 + return response +``` + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Flask (such as analytics, feature flags, A/B testing, etc.), have a look at our [Python SDK docs](/docs/libraries/python.md). + +Alternatively, the following tutorials can help you get started: + +- [How to set up analytics in Python and Flask](/tutorials/python-analytics.md) +- [How to set up feature flags in Python and Flask](/tutorials/python-feature-flags.md) +- [How to set up A/B tests in Python and Flask](/tutorials/python-ab-testing.md) + +## Supported versions + +These docs cover version `7.x` of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on `7.x.x` and higher — pin to the 6.x line with `pip install 'posthog<7'`, where `6.9.3` is the final release. + +Everything on this page works the same way on `6.9.3`. Event capture, the context API (`new_context`, `identify_context`, `set_context_session`), and `PosthogContextMiddleware` are identical on `6.9.3` and `7.0.0` — `7.0.0` only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the `X-POSTHOG-DISTINCT-ID` header and falling back to the authenticated user, which behaves the same across both lines. + +Later `7.x` releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and `set_context_device_id`. They also changed the middleware's own captured properties: `7.x` sends the request IP as `$ip`, where `6.9.3` sends it as `$ip_address`, and `7.x` additionally captures `$request_path`, `$raw_user_agent`, and the authenticated user's `email`. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/monitoring.md b/skills/posthog/all/skills/error-tracking-flask/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/python.md b/skills/posthog/all/skills/error-tracking-flask/references/python.md new file mode 100644 index 00000000..f442432b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/python.md @@ -0,0 +1,191 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Error Tracking installation - Docs + +Copy page + +# Python Error Tracking installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog Python library using pip: + + Terminal + + PostHog AI + + ```bash + pip install posthog + ``` + +2. 2 + + ## Initialize PostHog + + Required + + Initialize the PostHog client with your project token and host from your project settings: + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog( + project_api_key='', + host='https://us.i.posthog.com' + ) + ``` + + **Django integration** + + If you're using Django, check out our [Django integration](/docs/libraries/django.md) for automatic request tracking. + +3. 3 + + ## Send events + + Recommended + + Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration: + + Capture custom events by calling the `capture` method with an event name and properties: + + Python + + PostHog AI + + ```python + import posthog + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) + ``` + +4. ## Verify PostHog is initialized + + Recommended + + Before proceeding, enable debug and call `posthog.capture('test_event')` to make sure you can capture events. + +5. 4 + + ## Setting up exception autocapture + + Recommended + + Exception autocapture can be enabled during initialization of the PostHog client to automatically capture any unhandled exceptions thrown by your Python application. It works by setting Python's built-in exception hooks, such as `sys.excepthook` and `threading.excepthook`. + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog("", enable_exception_autocapture=True, ...) + ``` + + We recommend setting up and using [contexts](/docs/libraries/python.md#contexts) so that exceptions automatically include distinct IDs, session IDs, and other properties you can set up with tags. + + You can also enable [code variables capture](/docs/error-tracking/code-variables/python.md) to automatically capture the state of local variables when exceptions occur, giving you a debugger-like view of your application. + +6. 5 + + ## Manually capturing exceptions + + Optional + + For exceptions handled by your application that you would still like sent to PostHog, you can manually call the capture method: + + Python + + PostHog AI + + ```python + posthog.capture_exception(e, distinct_id="user_distinct_id", properties=additional_properties) + ``` + + You can find a full example of all of this in our [Python (and Flask) error tracking tutorial](/tutorials/python-error-tracking.md). + +7. 6 + + ## Framework-specific exception capture + + Optional + + Python frameworks often have built-in error handlers. This means PostHog's default exception autocapture won't work and we need to manually capture errors instead. The exact process depends on the framework: + + ## Django + + The Python SDK provides a Django middleware that automatically wraps all requests with a [context](/docs/libraries/python.md#contexts). Add the middleware to your Django settings: + + Python + + PostHog AI + + ```python + MIDDLEWARE = [ + # ... other middleware + 'posthog.integrations.django.PosthogContextMiddleware', + # ... other middleware + ] + ``` + + By default, the middleware captures exceptions and sends them to PostHog. Disable with `POSTHOG_MW_CAPTURE_EXCEPTIONS = False`. Use `POSTHOG_MW_EXTRA_TAGS`, `POSTHOG_MW_REQUEST_FILTER`, and `POSTHOG_MW_TAG_MAP` to customize. See the [Django integration docs](/docs/libraries/django.md) for full configuration. + + ## Flask + + Python + + PostHog AI + + ```python + from flask import Flask, jsonify + from posthog import Posthog + posthog = Posthog('', host='https://us.i.posthog.com') + @app.errorhandler(Exception) + def handle_exception(e): + event_id = posthog.capture_exception(e) + response = jsonify({'message': str(e), 'error_id': event_id}) + response.status_code = 500 + return response + ``` + + ## FastAPI + + Python + + PostHog AI + + ```python + from fastapi.responses import JSONResponse + from posthog import Posthog + posthog = Posthog('', host='https://us.i.posthog.com') + @app.exception_handler(Exception) + async def http_exception_handler(request, exc): + posthog.capture_exception(exc) + return JSONResponse(status_code=500, content={'message': str(exc)}) + ``` + +8. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flask/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-flask/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flask/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-flutter/SKILL.md b/skills/posthog/all/skills/error-tracking-flutter/SKILL.md index b020d3ee..76c90553 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-flutter/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-flutter description: PostHog error tracking for Flutter metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Flutter @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Flutter applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,13 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-flutter/references/COMMANDMENTS.md new file mode 100644 index 00000000..58624ee9 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-flutter/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/alerts.md b/skills/posthog/all/skills/error-tracking-flutter/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-flutter/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-flutter/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/flutter.md b/skills/posthog/all/skills/error-tracking-flutter/references/flutter.md index 86a79679..388ea1c3 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/flutter.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/flutter.md @@ -1,4 +1,10 @@ -# Flutter error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Flutter Error Tracking installation - Docs + +Copy page + +# Flutter Error Tracking installation - Docs 1. 1 @@ -13,7 +19,7 @@ PostHog AI ```yaml - posthog_flutter: ^5.0.0 + posthog_flutter: ^5.24.0 ``` 2. 2 @@ -35,7 +41,7 @@ [...] - + @@ -50,7 +56,7 @@ ```groovy defaultConfig { - minSdkVersion 21 + minSdkVersion 23 // rest of your config } ``` @@ -66,7 +72,7 @@ ```xml [...] - com.posthog.posthog.API_KEY + com.posthog.posthog.PROJECT_TOKEN com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com @@ -102,10 +108,10 @@ ... @@ -159,6 +165,7 @@ config.errorTrackingConfig.captureFlutterErrors = true; config.errorTrackingConfig.capturePlatformDispatcherErrors = true; config.errorTrackingConfig.captureIsolateErrors = true; + // Requires SDK version 5.22.0 or higher config.errorTrackingConfig.captureNativeExceptions = true; config.errorTrackingConfig.captureSilentFlutterErrors = false; await Posthog().setup(config); @@ -171,7 +178,7 @@ | captureFlutterErrors | Captures Flutter framework errors (FlutterError.onError) | | capturePlatformDispatcherErrors | Captures Dart runtime errors (PlatformDispatcher.onError). Web not supported. | | captureIsolateErrors | Captures errors from main isolate. Web not supported. | - | captureNativeExceptions | Captures native exceptions (Java/Kotlin exceptions). Android only. | + | captureNativeExceptions | Captures native exceptions. Android (Java/Kotlin) and Apple platforms (iOS, macOS, tvOS). | | captureSilentFlutterErrors | Captures Flutter errors that are marked as silent. Default: false. | 5. 5 @@ -247,12 +254,12 @@ We currently don't support the following features: - No de-obfuscating stacktraces from obfuscated builds ([\--obfuscate](https://docs.flutter.dev/deployment/obfuscate) and [\--split-debug-info](https://docs.flutter.dev/deployment/obfuscate)) for Dart code - - No de-obfuscating stacktraces when [isMinifyEnabled](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) is enabled for Java/Kotlin code - - No [Source code context](/docs/error-tracking/stack-traces.md) associated with an exception - - No native iOS exception capture + - No [Source code context](/docs/error-tracking/stack-traces.md) associated with an exception (native Android Java/Kotlin errors and Flutter web only) - No native C/C++ exception capture on Android (Java/Kotlin only) - No background isolate error capture + For symbolicated stack traces on native platforms, see the [Flutter debug symbols guide](/docs/error-tracking/upload-source-maps/flutter.md). + These features will be added in future releases. We recommend you stay up to date with the latest version of the PostHog Flutter SDK. 7. ## Verify error tracking @@ -279,9 +286,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/flutter.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/monitoring.md b/skills/posthog/all/skills/error-tracking-flutter/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-flutter/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-flutter/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-flutter/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-flutter/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/SKILL.md b/skills/posthog/all/skills/error-tracking-go/SKILL.md index 016c9c85..ba10036d 100644 --- a/skills/posthog/all/skills/error-tracking-go/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-go/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-go description: PostHog error tracking for Go metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Go @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Go applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,14 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/error-tracking-go/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-go/references/COMMANDMENTS.md new file mode 100644 index 00000000..68b721fe --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-go/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/error-tracking-go/references/alerts.md b/skills/posthog/all/skills/error-tracking-go/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-go/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-go/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-go/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-go/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-go/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/references/go.md b/skills/posthog/all/skills/error-tracking-go/references/go.md index 9ecf6d1a..895058cb 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/go.md +++ b/skills/posthog/all/skills/error-tracking-go/references/go.md @@ -1,4 +1,10 @@ -# Go error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Go Error Tracking installation - Docs + +Copy page + +# Go Error Tracking installation - Docs 1. 1 @@ -16,9 +22,9 @@ go get github.com/posthog/posthog-go ``` - **Source context not yet supported** + **Debug symbol uploads** - The Go SDK captures stack traces with file names, line numbers, and function names, but does not yet support source context (displaying the surrounding lines of code in the error tracking UI). Symbol set uploads for Go are not currently available. + The Go SDK resolves stack traces in-process, so captured frames include file names, line numbers, function names, and inlined calls without any symbol uploads. To also see source context (the surrounding lines of code in the error tracking UI), [upload debug symbols](/docs/error-tracking/upload-source-maps/go.md). That needs posthog-go 1.22.0 or later. 2. 2 @@ -106,6 +112,8 @@ client.Enqueue(exception) ``` + To see how `net/http` services can automatically associate backend exceptions with frontend users, view the [Go request context documentation](/docs/libraries/go.md#request-context). + ### Option B: Automatic capture with slog The SDK provides a `SlogCaptureHandler` that wraps Go's standard `log/slog` logger and automatically captures log records as exceptions. @@ -185,9 +193,9 @@ client.Close() ``` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/references/monitoring.md b/skills/posthog/all/skills/error-tracking-go/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-go/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-go/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-go/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-go/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-go/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/SKILL.md b/skills/posthog/all/skills/error-tracking-hono/SKILL.md index eef3f8a4..1cccc1f9 100644 --- a/skills/posthog/all/skills/error-tracking-hono/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-hono/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-hono description: PostHog error tracking for Hono metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Hono @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Hono applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-hono/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-hono/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-hono/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-hono/references/alerts.md b/skills/posthog/all/skills/error-tracking-hono/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-hono/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-hono/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/references/hono.md b/skills/posthog/all/skills/error-tracking-hono/references/hono.md index d0fe0b6c..4409ffbf 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/hono.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/hono.md @@ -1,4 +1,10 @@ -# Hono error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Hono Error Tracking installation - Docs + +Copy page + +# Hono Error Tracking installation - Docs 1. 1 @@ -128,9 +134,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/references/monitoring.md b/skills/posthog/all/skills/error-tracking-hono/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-hono/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-hono/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-hono/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-hono/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ios/SKILL.md b/skills/posthog/all/skills/error-tracking-ios/SKILL.md new file mode 100644 index 00000000..796975d3 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/SKILL.md @@ -0,0 +1,50 @@ +--- +name: error-tracking-ios +description: PostHog error tracking for iOS +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for iOS + +This skill helps you add PostHog error tracking to iOS applications. + +## Reference files + +- `references/ios.md` - Ios error tracking installation - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version +- If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/error-tracking-ios/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-ios/references/COMMANDMENTS.md new file mode 100644 index 00000000..dd2c05a5 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version +- If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/error-tracking-ios/references/alerts.md b/skills/posthog/all/skills/error-tracking-ios/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-ios/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-ios/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-ios/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-ios/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-ios/references/ios.md b/skills/posthog/all/skills/error-tracking-ios/references/ios.md new file mode 100644 index 00000000..c9b040d1 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/ios.md @@ -0,0 +1,264 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS Error Tracking installation - Docs + +Copy page + +# iOS Error Tracking installation - Docs + +1. 1 + + ## Install dependency + + Required + + Install via Swift Package Manager: + + Package.swift + + PostHog AI + + ```swift + dependencies: [ + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.56.0") + ] + ``` + + Or add PostHog to your Podfile: + + Podfile + + PostHog AI + + ```ruby + pod "PostHog", "~> 3.56" + ``` + +2. 2 + + ## Configure PostHog + + Required + + Initialize PostHog in your AppDelegate: + + AppDelegate.swift + + PostHog AI + + ```swift + import Foundation + import PostHog + import UIKit + class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + let POSTHOG_PROJECT_TOKEN = "" + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + } + ``` + +3. 3 + + ## Send events + + Recommended + + Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration: + + Swift + + PostHog AI + + ```swift + PostHogSDK.shared.capture("button_clicked", properties: ["button_name": "signup"]) + ``` + +4. 4 + + ## Set up exception autocapture + + Recommended + + **Remote configuration** + + Exception autocapture can also be managed remotely via the [error tracking settings](https://app.posthog.com/settings/project-error-tracking#exception-autocapture). + + **Platform support** + + Exception autocapture is available on **iOS, macOS, and tvOS** only. It is not available on watchOS or visionOS due to platform limitations. + + You can still capture events manually on all platforms, including visionOS. + + You can autocapture exceptions by setting the `errorTrackingConfig.autoCapture` argument to `true` when initializing the PostHog SDK. + + Swift + + PostHog AI + + ```swift + import PostHog + let config = PostHogConfig( + projectToken: "", + host: "https://us.i.posthog.com" + ) + config.errorTrackingConfig.autoCapture = true + PostHogSDK.shared.setup(config) + ``` + + When enabled, this automatically captures `$exception` events for: + + - **Mach exceptions** (e.g., `EXC_BAD_ACCESS`, `EXC_CRASH`) + - **POSIX signals** (e.g., `SIGSEGV`, `SIGABRT`, `SIGBUS`) + - **Uncaught NSExceptions** + + Crashes are persisted to disk and sent as `$exception` events with level "fatal" on the next app launch. + +5. 5 + + ## Manually capture exceptions + + Optional + + ### Swift Error handling + + You can manually capture exceptions using the `captureException` method: + + Swift + + PostHog AI + + ```swift + import PostHog + do { + try FileManager.default.removeItem(at: badFileUrl) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + + ### Objective-C NSException handling + + For Objective-C code that uses NSException: + + Objective-C + + PostHog AI + + ```objc + @import PostHog; + @try { + [self riskyOperation]; + } @catch (NSException *exception) { + [[PostHogSDK shared] captureExceptionWithNSException:exception properties:nil]; + } + ``` + + ### Adding custom properties + + You can add custom properties to help with debugging, grouping, and analysis: + + Swift + + PostHog AI + + ```swift + do { + try performNetworkRequest() + } catch { + PostHogSDK.shared.captureException(error, properties: [ + "endpoint": "/api/users", + "retry_count": 3 + ]) + } + ``` + + This is helpful if you've built your own error handling logic or want to capture exceptions that are handled by your application code. + +6. 6 + + ## Configure in-app frames + + Optional + + By default, PostHog automatically marks your app's code as "in-app" in stack traces to help you focus on your code rather than system frameworks. + + You can customize this behavior with `errorTrackingConfig`: + + Swift + + PostHog AI + + ```swift + import PostHog + let config = PostHogConfig( + projectToken: "", + host: "https://us.i.posthog.com" + ) + // Mark additional packages as in-app + config.errorTrackingConfig.inAppIncludes = [ + "MySharedFramework", + "MyUtilityLib" + ] + // Exclude specific packages from being marked as in-app + config.errorTrackingConfig.inAppExcludes = [ + "Alamofire", + "SDWebImage" + ] + // Control default behavior for unknown packages + config.errorTrackingConfig.inAppByDefault = true // default + PostHogSDK.shared.setup(config) + ``` + + **Configuration options:** + + | Option | Description | + | --- | --- | + | inAppIncludes | List of package/bundle identifiers to mark as in-app (takes precedence over excludes) | + | inAppExcludes | List of package/bundle identifiers to exclude from in-app | + | inAppByDefault | Whether frames are considered in-app by default when origin cannot be determined | + + **Default behavior:** + + - Your app's bundle identifier and executable name are automatically included + - System frameworks (Foundation, UIKit, etc.) are automatically excluded + +7. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +8. 7 + + ## Upload dSYMs + + Required + + Great, you're capturing exceptions! The next step is to upload dSYM files so PostHog can symbolicate your crash reports and generate accurate stack traces. + + Let's continue to the next section. + + [Upload dSYMs](/docs/error-tracking/upload-source-maps/ios.md) + +## Limitations: + +- System symbols and frames are not symbolicated (UIKit, Foundation, etc.) ([issue](https://github.com/PostHog/posthog/issues/50614)). +- Swift crashes appear as `SIGTRAP` without the actual error message ([issue](https://github.com/PostHog/posthog-ios/issues/522)). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-ios/references/monitoring.md b/skills/posthog/all/skills/error-tracking-ios/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-ios/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-ios/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ios/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/SKILL.md b/skills/posthog/all/skills/error-tracking-laravel/SKILL.md new file mode 100644 index 00000000..45400fdf --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/SKILL.md @@ -0,0 +1,46 @@ +--- +name: error-tracking-laravel +description: PostHog error tracking for Laravel +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for Laravel + +This skill helps you add PostHog error tracking to Laravel applications. + +## Reference files + +- `references/php.md` - Php error tracking installation - docs +- `references/laravel.md` - Laravel - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Create a dedicated PostHogService class in app/Services/ - do NOT scatter PostHog::capture calls throughout controllers +- Register PostHog configuration in config/posthog.php using env() for all settings (api_key, host, disabled) +- Do NOT use Laravel's event system or observers for analytics - call capture explicitly where actions occur +- Call PostHog::flush() after capture in queue jobs, Horizon, and Octane - a long-running worker never destructs, so its events sit in the SDK buffer until batch_size (default 100) is reached and are silently lost on restart +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-laravel/references/COMMANDMENTS.md new file mode 100644 index 00000000..80ab83ad --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Create a dedicated PostHogService class in app/Services/ - do NOT scatter PostHog::capture calls throughout controllers +- Register PostHog configuration in config/posthog.php using env() for all settings (api_key, host, disabled) +- Do NOT use Laravel's event system or observers for analytics - call capture explicitly where actions occur +- Call PostHog::flush() after capture in queue jobs, Horizon, and Octane - a long-running worker never destructs, so its events sit in the SDK buffer until batch_size (default 100) is reached and are silently lost on restart +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/alerts.md b/skills/posthog/all/skills/error-tracking-laravel/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-laravel/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-laravel/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/laravel.md b/skills/posthog/all/skills/error-tracking-laravel/references/laravel.md new file mode 100644 index 00000000..830063b9 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/laravel.md @@ -0,0 +1,176 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Laravel - Docs + +Copy page + +# Laravel - Docs + +PostHog integrates with Laravel through the [PostHog PHP SDK](/docs/libraries/php.md). This page covers Laravel-specific setup. For SDK features such as event capture, identifying users, feature flags, group analytics, and configuration options, see the [PHP SDK docs](/docs/libraries/php.md). + +## Installation + +Install the PHP SDK as described in the [PHP installation guide](/docs/libraries/php.md#installation), then add your project token and host to `.env`: + +.env + +PostHog AI + +```bash +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com +``` + +Add PostHog to Laravel's services config: + +config/services.php + +PostHog AI + +```php +'posthog' => [ + 'api_key' => env('POSTHOG_API_KEY'), + 'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'), +], +``` + +Initialize PostHog in the `boot` method of `app/Providers/AppServiceProvider.php`: + +app/Providers/AppServiceProvider.php + +PostHog AI + +```php + config('services.posthog.host'), + ] + ); + } +} +``` + +## Request context middleware + +Client SDKs such as [PostHog JS](/docs/libraries/js.md) can send tracing headers to your Laravel backend. Configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Laravel backend hostname so browser requests include the session and distinct ID headers. + +The PHP SDK can read `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` headers and apply them to events captured during the request. Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side events or decisions, pass an authenticated `distinctId` explicitly, such as `auth()->id()`. For the lower-level context APIs, see the [PHP request context docs](/docs/libraries/php.md#request-context). + +Add middleware like this: + +app/Http/Middleware/PostHogRequestContext.php + +PostHog AI + +```php +headers->all()); + $context['properties'] = array_merge( + $context['properties'] ?? [], + array_filter([ + '$current_url' => $request->fullUrl(), + '$request_method' => $request->method(), + '$request_path' => $request->getPathInfo(), + '$user_agent' => $request->userAgent(), + '$ip' => $request->ip(), + ], static fn ($value): bool => $value !== null && $value !== '') + ); + return PostHog::withContext( + $context, + static fn (): Response => $next($request), + ['fresh' => true] + ); + } +} +``` + +Register this middleware using your Laravel version's normal middleware registration. + +## Error tracking in Laravel + +The PHP SDK supports [error tracking](/docs/libraries/php.md#error-tracking), but Laravel handles most request exceptions before they become uncaught PHP exceptions. Capture Laravel-reported exceptions explicitly. + +In Laravel 11 and later, add a report callback in `bootstrap/app.php`: + +bootstrap/app.php + +PostHog AI + +```php +use Illuminate\Foundation\Configuration\Exceptions; +use PostHog\PostHog; +use Throwable; +->withExceptions(function (Exceptions $exceptions): void { + $exceptions->report(function (Throwable $e): void { + if (! config('services.posthog.api_key')) { + return; + } + PostHog::captureException( + $e, + auth()->id() !== null ? (string) auth()->id() : null, + [ + '$current_url' => request()->fullUrl(), + '$request_method' => request()->method(), + ] + ); + }); +}) +``` + +For older Laravel versions, call `PostHog::captureException()` from your exception handler's `report` method. + +## Long-running processes + +In normal PHP request lifecycles, queued events flush when the client is destroyed. In long-running Laravel processes such as queue workers, Horizon, or Octane, call `PostHog::flush()` after capturing important events or at the end of a job/request. + +If you prefer immediate delivery in queue workers, configure the PHP SDK with `batch_size` set to `1` for those workers: + +PHP + +PostHog AI + +```php +PostHog::init( + '', + [ + 'host' => config('services.posthog.host'), + 'batch_size' => 1, + ] +); +``` + +## Next steps + +See the [PHP SDK docs](/docs/libraries/php.md) for usage examples and the full API reference. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/monitoring.md b/skills/posthog/all/skills/error-tracking-laravel/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/php.md b/skills/posthog/all/skills/error-tracking-laravel/references/php.md new file mode 100644 index 00000000..7a3242cd --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/php.md @@ -0,0 +1,228 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Error Tracking installation - Docs + +Copy page + +# PHP Error Tracking installation - Docs + +1. 1 + + ## Install the PHP SDK + + Required + + Install the [PostHog PHP SDK](/docs/libraries/php.md) via Composer: + + Terminal + + PostHog AI + + ```bash + composer require posthog/posthog-php + ``` + +2. 2 + + ## Initialize the client + + Required + + Set your project token and instance address before making any calls: + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + ['host' => 'https://us.i.posthog.com'] + ); + ``` + + You can find your project token and instance address in the [project settings](https://app.posthog.com/settings/project) page in PostHog. + +3. 3 + + ## Capture exceptions + + Required + + Use `captureException` to manually capture exceptions and send them to PostHog as `$exception` events with full stack traces. + + ### Basic usage + + PHP + + PostHog AI + + ```php + try { + // Your code that might throw + riskyOperation(); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id'); + } + ``` + + ### With additional properties + + You can pass extra properties to include with the exception event: + + PHP + + PostHog AI + + ```php + try { + processOrder($orderId); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id', [ + 'order_id' => $orderId, + 'environment' => 'production', + ]); + } + ``` + + You can also pass a plain string if you want to send an error message without a `Throwable`. + +4. 4 + + ## Enable automatic capture + + Recommended + + Automatic capture is opt-in for PHP. When enabled, the SDK installs handlers for uncaught exceptions. With the default `capture_errors: true`, it also captures PHP errors and fatal shutdown errors. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + ``` + + **Existing handlers are preserved** + + The SDK chains existing exception and error handlers instead of replacing your app's behavior. + +5. 5 + + ## Identify users and attach request context + + Recommended + + By default, automatically captured errors are anonymous. Use `context_provider` to attach a `distinctId` and request metadata to every automatically captured error event. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [ + '$current_url' => $_SERVER['REQUEST_URI'] ?? null, + '$request_method' => $_SERVER['REQUEST_METHOD'] ?? null, + '$exception_source' => $payload['source'] ?? null, + ], + ]; + }, + ], + ] + ); + ``` + + If `distinctId` is omitted, PostHog sends the event with an auto-generated ID and sets `$process_person_profile` to `false`. + +6. 6 + + ## Configure error tracking options + + Optional + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'capture_errors' => true, + 'excluded_exceptions' => [ + \InvalidArgumentException::class, + ], + 'max_frames' => 20, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [], + ]; + }, + ], + ] + ); + ``` + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | enabled | boolean | false | Enables automatic error tracking handlers. Manual captureException works regardless. | + | capture_errors | boolean | true | When enabled, also captures PHP errors and fatal shutdown errors in addition to uncaught exceptions. | + | excluded_exceptions | array of class strings | [] | Throwable classes to skip during automatic capture. | + | max_frames | integer | 20 | Maximum number of stack frames included in $exception_list. | + | context_provider | callable or null | null | Callback that returns distinctId and extra event properties for automatic captures. | + +7. ## Verify error tracking + + Recommended + + Trigger a test exception to confirm events are being sent to PostHog. You should see them appear in the [Error Tracking](https://app.posthog.com/error_tracking) tab. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + try { + throw new \Exception('Test exception from PHP'); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'test_user'); + } + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-laravel/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-laravel/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-laravel/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-nextjs/SKILL.md b/skills/posthog/all/skills/error-tracking-nextjs/SKILL.md index 783299ad..234ff13b 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-nextjs description: PostHog error tracking for Next.js metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Next.js @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Next.js applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup - For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically - Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes @@ -40,3 +42,6 @@ Consult the documentation for API details and framework-specific patterns. - Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler - To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect - useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-nextjs/references/COMMANDMENTS.md new file mode 100644 index 00000000..d9b85301 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/COMMANDMENTS.md @@ -0,0 +1,17 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/alerts.md b/skills/posthog/all/skills/error-tracking-nextjs/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-nextjs/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-nextjs/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/monitoring.md b/skills/posthog/all/skills/error-tracking-nextjs/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/nextjs.md b/skills/posthog/all/skills/error-tracking-nextjs/references/nextjs.md index 0834e9ae..0cc37a26 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/nextjs.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/nextjs.md @@ -1,4 +1,10 @@ -# Next.js error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Next.js Error Tracking installation - Docs + +Copy page + +# Next.js Error Tracking installation - Docs 1. 1 @@ -65,7 +71,7 @@ import posthog from 'posthog-js' posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30' + defaults: '2026-05-30' }) ``` @@ -82,12 +88,12 @@ import { usePathname, useSearchParams } from "next/navigation" import { useEffect } from "react" import posthog from 'posthog-js' - import { PostHogProvider as PHProvider } from 'posthog-js/react' + import { PostHogProvider as PHProvider } from '@posthog/react' export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN as string, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30' + defaults: '2026-05-30' }) }, []) return ( @@ -132,13 +138,13 @@ import { useEffect } from 'react' import { Router } from 'next/router' import posthog from 'posthog-js' - import { PostHogProvider } from 'posthog-js/react' + import { PostHogProvider } from '@posthog/react' import type { AppProps } from 'next/app' export default function App({ Component, pageProps }: AppProps) { useEffect(() => { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN as string, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30', + defaults: '2026-05-30', loaded: (posthog) => { if (process.env.NODE_ENV === 'development') posthog.debug() } @@ -191,7 +197,7 @@ ```typescript 'use client' - import { usePostHog } from 'posthog-js/react' + import { usePostHog } from '@posthog/react' export default function CheckoutPage() { const posthog = usePostHog() function handlePurchase() { @@ -414,7 +420,7 @@ Importantly, you need to: - 1. Set up a `posthog-node` client in your server-side code. See our doc on [setting up Next.js server-side analytics](/docs/libraries/next-js.md#server-side-analytics.md) for more. + 1. Set up a `posthog-node` client in your server-side code. See our doc on [setting up Next.js server-side analytics](/docs/libraries/next-js.md#server-side-analytics) for more. 2. Check the request is running in the `nodejs` runtime to ensure PostHog works. You can call `posthog.debug()` to get verbose logging. 3. Get the `distinct_id` from the cookie to connect the error to a specific user. @@ -481,9 +487,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/nextjs.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nextjs/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-nextjs/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-nextjs/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-nextjs/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/SKILL.md b/skills/posthog/all/skills/error-tracking-node/SKILL.md index e3ff2fc4..c42f383d 100644 --- a/skills/posthog/all/skills/error-tracking-node/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-node/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-node description: PostHog error tracking for Node.js metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Node.js @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Node.js applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,12 +32,14 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -- posthog-node is the Node.js server-side SDK package name – do NOT use posthog-js on the server +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead - 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 - Remember that source code is available in the node_modules directory - Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-node/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-node/references/COMMANDMENTS.md new file mode 100644 index 00000000..11206d59 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-node/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead +- 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')) +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-node/references/alerts.md b/skills/posthog/all/skills/error-tracking-node/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-node/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-node/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-node/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-node/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-node/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/references/monitoring.md b/skills/posthog/all/skills/error-tracking-node/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-node/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/references/node.md b/skills/posthog/all/skills/error-tracking-node/references/node.md index 6e843e31..e903a026 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/node.md +++ b/skills/posthog/all/skills/error-tracking-node/references/node.md @@ -1,4 +1,10 @@ -# Node.js error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Node.js Error Tracking installation - Docs + +Copy page + +# Node.js Error Tracking installation - Docs 1. 1 @@ -151,9 +157,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/node.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-node/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-node/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-node/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-node/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nuxt/SKILL.md b/skills/posthog/all/skills/error-tracking-nuxt/SKILL.md index 877e7b49..c7ca1c93 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-nuxt description: PostHog error tracking for Nuxt metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Nuxt @@ -12,12 +12,14 @@ This skill helps you add PostHog error tracking to Nuxt applications. ## Reference files -- `references/nuxt.md` - Nuxt error tracking installation (v3.7 and above) - docs +- `references/nuxt-3-7.md` - Nuxt error tracking installation (v3.7 and above) - docs +- `references/nuxt-3-6.md` - Nuxt error tracking installation (v3.6 and below) - docs - `references/fingerprints.md` - Fingerprints - docs - `references/alerts.md` - Send error tracking alerts - docs - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +33,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-nuxt/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/alerts.md b/skills/posthog/all/skills/error-tracking-nuxt/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-nuxt/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-nuxt/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/monitoring.md b/skills/posthog/all/skills/error-tracking-nuxt/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-6.md b/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-6.md new file mode 100644 index 00000000..faef300e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-6.md @@ -0,0 +1,257 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Nuxt Error Tracking installation (v3.6 and below) - Docs + +Copy page + +# Nuxt Error Tracking installation (v3.6 and below) - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog JavaScript library using your package manager: + + PostHog AI + + ### npm + + ```bash + npm install posthog-js + ``` + + ### yarn + + ```bash + yarn add posthog-js + ``` + + ### pnpm + + ```bash + pnpm add posthog-js + ``` + + **Nuxt version** + + This guide is for Nuxt v3.0 and above. For Nuxt v2.16 and below, see our [Nuxt docs](/docs/libraries/nuxt-js.md#nuxt-v216-and-below). + +2. 2 + + ## Add environment variables + + Required + + Add your PostHog project token and host to your `nuxt.config.js` file: + + nuxt.config.js + + PostHog AI + + ```javascript + export default defineNuxtConfig({ + runtimeConfig: { + public: { + posthogPublicKey: '', + posthogHost: 'https://us.i.posthog.com', + posthogDefaults: '2026-05-30' + } + } + }) + ``` + +3. 3 + + ## Create a plugin + + Required + + Create a new plugin by creating a new file `posthog.client.js` in your plugins directory: + + plugins/posthog.client.js + + PostHog AI + + ```javascript + import { defineNuxtPlugin } from '#app' + import posthog from 'posthog-js' + export default defineNuxtPlugin(nuxtApp => { + const runtimeConfig = useRuntimeConfig(); + const posthogClient = posthog.init(runtimeConfig.public.posthogPublicKey, { + api_host: runtimeConfig.public.posthogHost, + defaults: runtimeConfig.public.posthogDefaults, + loaded: (posthog) => { + if (import.meta.env.MODE === 'development') posthog.debug(); + } + }) + return { + provide: { + posthog: () => posthogClient + } + } + }) + ``` + +4. 4 + + ## Server-side setup + + Optional + + To capture events from server routes, install `posthog-node` and instantiate it directly. You can also use it to evaluate feature flags on the server: + + PostHog AI + + ### npm + + ```bash + npm install posthog-node + ``` + + ### yarn + + ```bash + yarn add posthog-node + ``` + + ### pnpm + + ```bash + pnpm add posthog-node + ``` + + server/api/example.js + + PostHog AI + + ```javascript + import { PostHog } from 'posthog-node' + export default defineEventHandler(async (event) => { + const runtimeConfig = useRuntimeConfig() + const posthog = new PostHog( + runtimeConfig.public.posthogPublicKey, + { host: runtimeConfig.public.posthogHost } + ) + posthog.capture({ + distinctId: 'distinct_id_of_the_user', + event: 'event_name' + }) + await posthog.shutdown() + }) + ``` + +5. 5 + + ## Send events + + Click around and view a couple pages to generate some events. PostHog automatically captures pageviews, clicks, and other interactions for you. + + If you'd like, you can also manually capture custom events: + + JavaScript + + PostHog AI + + ```javascript + posthog.capture('my_custom_event', { property: 'value' }) + ``` + +6. 6 + + ## Manually capturing exceptions + + Optional + + To send errors directly using the PostHog client, import it and use the `captureException` method like this: + + Vue + + PostHog AI + + ```html + + ``` + + On the server side, you can use the `posthog` object directly. + + server/api/example.js + + PostHog AI + + ```javascript + const runtimeConfig = useRuntimeConfig() + const posthog = new PostHog( + runtimeConfig.public.posthogPublicKey, + { + host: runtimeConfig.public.posthogHost, + } + ); + try { + const results = await DB.query.users.findMany() + return results + } catch (error) { + posthog.captureException(error) + } + ``` + +7. 7 + + ## Configuring exception autocapture + + Recommended + + Update your `posthog.client.js` to add an error hook. + + JavaScript + + PostHog AI + + ```javascript + export default defineNuxtPlugin((nuxtApp) => { + ... + nuxtApp.hook('vue:error', (error) => { + posthogClient.captureException(error) + }) + ... + }) + ``` + +8. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +9. 8 + + ## Upload source maps + + Required + + Great, you're capturing exceptions! If you serve minified bundles, the next step is to upload source maps to generate accurate stack traces. + + Let's continue to the next section. + + [Upload source maps](/docs/error-tracking/upload-source-maps/nuxt.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-7.md b/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-7.md new file mode 100644 index 00000000..2a0eb7aa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/nuxt-3-7.md @@ -0,0 +1,187 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Nuxt Error Tracking installation (v3.7 and above) - Docs + +Copy page + +# Nuxt Error Tracking installation (v3.7 and above) - Docs + +1. 1 + + ## Install the PostHog Nuxt module + + Required + + Install the PostHog Nuxt module using your package manager: + + PostHog AI + + ### npm + + ```bash + npm install @posthog/nuxt + ``` + + ### yarn + + ```bash + yarn add @posthog/nuxt + ``` + + ### pnpm + + ```bash + pnpm add @posthog/nuxt + ``` + + ### bun + + ```bash + bun add @posthog/nuxt + ``` + + Add the module to your `nuxt.config.ts` file: + + nuxt.config.ts + + PostHog AI + + ```typescript + export default defineNuxtConfig({ + modules: ['@posthog/nuxt'], + // Enable source maps generation in both vue and nitro + sourcemap: { + client: 'hidden' + }, + nitro: { + rollupConfig: { + output: { + sourcemapExcludeSources: false, + }, + }, + }, + posthogConfig: { + publicKey: '', // Find it in project settings https://app.posthog.com/settings/project + host: 'https://us.i.posthog.com', // Optional: defaults to https://us.i.posthog.com. Use https://eu.i.posthog.com for EU region + clientConfig: { + capture_exceptions: true, // Enables automatic exception capture on the client side (Vue) + }, + serverConfig: { + enableExceptionAutocapture: true, // Enables automatic exception capture on the server side (Nitro) + }, + sourcemaps: { + enabled: true, + projectId: '', // Your project ID, found in your environment settings: https://app.posthog.com/settings/environment#variables + personalApiKey: '', // Your personal API key from PostHog settings https://app.posthog.com/settings/user-api-keys (requires organization:read and error_tracking:write scopes) + releaseName: 'my-application', // Optional: defaults to git repository name + releaseVersion: '1.0.0', // Optional: defaults to current git commit + }, + }, + }) + ``` + + **Personal API key** + + Your personal API key will require `organization:read` and `error_tracking:write` scopes. + + The module will automatically: + + - Initialize PostHog on both Vue (client side) and Nitro (server side) + - Capture exceptions on both client and server + - Generate and upload source maps during build + +2. 2 + + ## Manually capturing exceptions + + Optional + + Our module if set up as shown above already captures both client and server side exceptions automatically. + + To send errors manually on the client side, import it and use the `captureException` method like this: + + Vue + + PostHog AI + + ```html + + ``` + + On the server side instantiate PostHog using: + + server/api/example.js + + PostHog AI + + ```javascript + const runtimeConfig = useRuntimeConfig() + const posthog = new PostHog( + runtimeConfig.public.posthogPublicKey, + { + host: runtimeConfig.public.posthogHost, + } + ); + try { + const results = await DB.query.users.findMany() + return results + } catch (error) { + posthog.captureException(error) + } + ``` + +3. 3 + + ## Build your project for production + + Required + + Build your project for production by running the following command: + + Terminal + + PostHog AI + + ```bash + nuxt build + ``` + + The PostHog module will automatically **generate and upload source maps** to PostHog during the build process. + +4. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +5. 4 + + ## Upload source maps + + Required + + Great, you're capturing exceptions! If you serve minified bundles, the next step is to upload source maps to generate accurate stack traces. + + Let's continue to the next section. + + [Upload source maps](/docs/error-tracking/upload-source-maps/nuxt.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-nuxt/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-nuxt/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-nuxt/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-nuxt/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-php/SKILL.md b/skills/posthog/all/skills/error-tracking-php/SKILL.md new file mode 100644 index 00000000..d59f3ae4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/SKILL.md @@ -0,0 +1,41 @@ +--- +name: error-tracking-php +description: PostHog error tracking for PHP +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for PHP + +This skill helps you add PostHog error tracking to PHP applications. + +## Reference files + +- `references/php.md` - Php error tracking installation - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-php/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-php/references/COMMANDMENTS.md new file mode 100644 index 00000000..71c9848e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/COMMANDMENTS.md @@ -0,0 +1,11 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-php/references/alerts.md b/skills/posthog/all/skills/error-tracking-php/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-php/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-php/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-php/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-php/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-php/references/monitoring.md b/skills/posthog/all/skills/error-tracking-php/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-php/references/php.md b/skills/posthog/all/skills/error-tracking-php/references/php.md new file mode 100644 index 00000000..7a3242cd --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/php.md @@ -0,0 +1,228 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Error Tracking installation - Docs + +Copy page + +# PHP Error Tracking installation - Docs + +1. 1 + + ## Install the PHP SDK + + Required + + Install the [PostHog PHP SDK](/docs/libraries/php.md) via Composer: + + Terminal + + PostHog AI + + ```bash + composer require posthog/posthog-php + ``` + +2. 2 + + ## Initialize the client + + Required + + Set your project token and instance address before making any calls: + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + ['host' => 'https://us.i.posthog.com'] + ); + ``` + + You can find your project token and instance address in the [project settings](https://app.posthog.com/settings/project) page in PostHog. + +3. 3 + + ## Capture exceptions + + Required + + Use `captureException` to manually capture exceptions and send them to PostHog as `$exception` events with full stack traces. + + ### Basic usage + + PHP + + PostHog AI + + ```php + try { + // Your code that might throw + riskyOperation(); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id'); + } + ``` + + ### With additional properties + + You can pass extra properties to include with the exception event: + + PHP + + PostHog AI + + ```php + try { + processOrder($orderId); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id', [ + 'order_id' => $orderId, + 'environment' => 'production', + ]); + } + ``` + + You can also pass a plain string if you want to send an error message without a `Throwable`. + +4. 4 + + ## Enable automatic capture + + Recommended + + Automatic capture is opt-in for PHP. When enabled, the SDK installs handlers for uncaught exceptions. With the default `capture_errors: true`, it also captures PHP errors and fatal shutdown errors. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + ``` + + **Existing handlers are preserved** + + The SDK chains existing exception and error handlers instead of replacing your app's behavior. + +5. 5 + + ## Identify users and attach request context + + Recommended + + By default, automatically captured errors are anonymous. Use `context_provider` to attach a `distinctId` and request metadata to every automatically captured error event. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [ + '$current_url' => $_SERVER['REQUEST_URI'] ?? null, + '$request_method' => $_SERVER['REQUEST_METHOD'] ?? null, + '$exception_source' => $payload['source'] ?? null, + ], + ]; + }, + ], + ] + ); + ``` + + If `distinctId` is omitted, PostHog sends the event with an auto-generated ID and sets `$process_person_profile` to `false`. + +6. 6 + + ## Configure error tracking options + + Optional + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'capture_errors' => true, + 'excluded_exceptions' => [ + \InvalidArgumentException::class, + ], + 'max_frames' => 20, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [], + ]; + }, + ], + ] + ); + ``` + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | enabled | boolean | false | Enables automatic error tracking handlers. Manual captureException works regardless. | + | capture_errors | boolean | true | When enabled, also captures PHP errors and fatal shutdown errors in addition to uncaught exceptions. | + | excluded_exceptions | array of class strings | [] | Throwable classes to skip during automatic capture. | + | max_frames | integer | 20 | Maximum number of stack frames included in $exception_list. | + | context_provider | callable or null | null | Callback that returns distinctId and extra event properties for automatic captures. | + +7. ## Verify error tracking + + Recommended + + Trigger a test exception to confirm events are being sent to PostHog. You should see them appear in the [Error Tracking](https://app.posthog.com/error_tracking) tab. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + try { + throw new \Exception('Test exception from PHP'); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'test_user'); + } + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-php/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-php/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-php/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-python/SKILL.md b/skills/posthog/all/skills/error-tracking-python/SKILL.md index 23ea5830..5e8b51dd 100644 --- a/skills/posthog/all/skills/error-tracking-python/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-python/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-python description: PostHog error tracking for Python metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Python @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Python applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Remember that source code is available in the venv/site-packages directory - posthog is the Python SDK package name - Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands diff --git a/skills/posthog/all/skills/error-tracking-python/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-python/references/COMMANDMENTS.md new file mode 100644 index 00000000..c8ef6d3b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-python/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/error-tracking-python/references/alerts.md b/skills/posthog/all/skills/error-tracking-python/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-python/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-python/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-python/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-python/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-python/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-python/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-python/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-python/references/monitoring.md b/skills/posthog/all/skills/error-tracking-python/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-python/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-python/references/python.md b/skills/posthog/all/skills/error-tracking-python/references/python.md index 49fbdad2..f442432b 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/python.md +++ b/skills/posthog/all/skills/error-tracking-python/references/python.md @@ -1,4 +1,10 @@ -# Python error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Error Tracking installation - Docs + +Copy page + +# Python Error Tracking installation - Docs 1. 1 @@ -56,7 +62,7 @@ ```python import posthog - posthog.capture('user_123', 'user_signed_up', properties={'example_property': 'example_value'}) + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) ``` 4. ## Verify PostHog is initialized @@ -176,9 +182,9 @@ [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-python/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-python/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-python/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-python/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/SKILL.md b/skills/posthog/all/skills/error-tracking-react-native/SKILL.md index 73d5db33..937726a5 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-react-native/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-react-native description: PostHog error tracking for React Native metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for React Native @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to React Native applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,7 +32,11 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - posthog-react-native is the React Native SDK package name - Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) - react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it - Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-react-native/references/COMMANDMENTS.md new file mode 100644 index 00000000..a1d09803 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-react-native/references/COMMANDMENTS.md @@ -0,0 +1,12 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-react-native is the React Native SDK package name +- Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) +- react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it +- Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/alerts.md b/skills/posthog/all/skills/error-tracking-react-native/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-react-native/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-react-native/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/monitoring.md b/skills/posthog/all/skills/error-tracking-react-native/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/react-native.md b/skills/posthog/all/skills/error-tracking-react-native/references/react-native.md index a83d991e..cbd7c200 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/react-native.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/react-native.md @@ -1,4 +1,10 @@ -# React Native error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# React Native Error Tracking installation - Docs + +Copy page + +# React Native Error Tracking installation - Docs 1. 1 @@ -108,6 +114,7 @@ uncaughtExceptions: true, unhandledRejections: true, console: ['error', 'warn'], + nativeCrashes: true, // native iOS/Android crashes (see below) }, }, }) @@ -120,6 +127,15 @@ | uncaughtExceptions | Captures Uncaught exceptions (ReactNativeGlobal.ErrorUtils.setGlobalHandler) | | unhandledRejections | Captures Unhandled rejections (ReactNativeGlobal.onunhandledrejection) | | console | Captures console logs as errors according to the reported LogLevel | + | nativeCrashes | Captures native iOS/Android crashes. Requires @posthog/react-native-plugin and uploaded native symbols (see below) | + + **Capturing native crashes** + + `nativeCrashes` captures native iOS and Android crashes that the JavaScript layer can't see. Beyond the config above, it needs: + + 1. The optional native plugin installed — `npx expo install @posthog/react-native-plugin` (Expo) or `npm i @posthog/react-native-plugin` (bare React Native). If it's missing, native capture is a no-op and your JS-level autocapture is unaffected. + 2. Your project's **Enable exception autocapture** setting enabled in [error tracking settings](https://app.posthog.com/settings/project-error-tracking#exception-autocapture) — the same server-side setting that gates JavaScript autocapture. + 3. Native debug symbols uploaded at build time, so crash stack traces are readable. See [native crash symbolication](/docs/error-tracking/upload-source-maps/react-native.md#native-crash-symbolication). 5. 5 @@ -197,10 +213,9 @@ We currently don't support the following features: - - No native Android and iOS exception capture - No automatic source map uploads on React Native web - These features will be added in future releases. We recommend you stay up to date with the latest version of the PostHog React Native SDK. + This will be added in a future release. We recommend you stay up to date with the latest version of the PostHog React Native SDK. 8. ## Verify error tracking @@ -216,19 +231,19 @@ 9. 8 - ## Upload source maps + ## Upload source maps & native symbols Required - Great, you're capturing exceptions! If you serve minified bundles, the next step is to upload source maps to generate accurate stack traces. + Great, you're capturing exceptions! The next step is to upload source maps (for JavaScript stack traces) and native symbols (for native iOS/Android crash symbolication) so PostHog can generate accurate stack traces. Let's continue to the next section. - [Upload source maps](/docs/error-tracking/upload-source-maps/react-native.md) + [Upload source maps & native symbols](/docs/error-tracking/upload-source-maps/react-native.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react-native/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-react-native/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-react-native/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-react-native/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/SKILL.md b/skills/posthog/all/skills/error-tracking-react/SKILL.md index a46a01d0..c10efd95 100644 --- a/skills/posthog/all/skills/error-tracking-react/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-react/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-react description: PostHog error tracking for React metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for React @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to React applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically - Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes - Do NOT use useEffect for data transformation - calculate derived values during render instead @@ -39,3 +41,6 @@ Consult the documentation for API details and framework-specific patterns. - Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler - To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect - useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-react/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-react/references/COMMANDMENTS.md new file mode 100644 index 00000000..cc803992 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-react/references/COMMANDMENTS.md @@ -0,0 +1,16 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-react/references/alerts.md b/skills/posthog/all/skills/error-tracking-react/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-react/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-react/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-react/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-react/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-react/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/references/monitoring.md b/skills/posthog/all/skills/error-tracking-react/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-react/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/references/react.md b/skills/posthog/all/skills/error-tracking-react/references/react.md index 88351b00..a846f574 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/react.md +++ b/skills/posthog/all/skills/error-tracking-react/references/react.md @@ -1,4 +1,10 @@ -# React error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# React Error Tracking installation - Docs + +Copy page + +# React Error Tracking installation - Docs 1. 1 @@ -34,15 +40,15 @@ Required - Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_PUBLIC_` prefix: + Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_` prefix to expose them to the client: .env PostHog AI ```bash - VITE_PUBLIC_POSTHOG_PROJECT_TOKEN= - VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + VITE_POSTHOG_PROJECT_TOKEN= + VITE_POSTHOG_HOST=https://us.i.posthog.com ``` 3. 3 @@ -64,12 +70,12 @@ import App from './App.jsx' import { PostHogProvider } from '@posthog/react' const options = { - api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30', + api_host: import.meta.env.VITE_POSTHOG_HOST, + defaults: '2026-05-30', } as const createRoot(document.getElementById('root')).render( - + @@ -214,9 +220,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/react.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-react/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-react/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-react/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-react/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/SKILL.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/SKILL.md index 5b4f71b2..35a5572e 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-ruby-on-rails description: PostHog error tracking for Ruby on Rails metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Ruby on Rails @@ -13,11 +13,13 @@ This skill helps you add PostHog error tracking to Ruby on Rails applications. ## Reference files - `references/ruby-on-rails.md` - Ruby on rails error tracking installation - docs +- `references/ruby-on-rails.md` - Ruby on rails - docs - `references/fingerprints.md` - Fingerprints - docs - `references/alerts.md` - Send error tracking alerts - docs - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +33,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Use posthog-rails gem alongside posthog-ruby for automatic exception capture and ActiveJob instrumentation - Run `rails generate posthog:install` to create the initializer, or manually create config/initializers/posthog.rb - Configure auto_capture_exceptions: true to automatically track unhandled exceptions in controllers @@ -41,7 +44,7 @@ Consult the documentation for API details and framework-specific patterns. - capture_exception takes POSITIONAL args: PostHog.capture_exception(exception, distinct_id, additional_properties) — do NOT use keyword args - Define posthog_distinct_id on the User model for automatic user association in error reports — posthog-rails auto-detects by trying: posthog_distinct_id, distinct_id, id, pk, uuid (in order) - For ActiveJob user association, use the class-level DSL `posthog_distinct_id ->(user) { user.email }` or pass user_id: in a hash argument -- Store API key in Rails credentials or environment variables, never hardcode +- Store the project token in Rails credentials or environment variables, never hardcode - For frontend tracking alongside posthog-rails, add the posthog-js snippet to the layout template — posthog-js handles pageviews, session replay, and client-side errors while posthog-ruby handles backend events, server errors, feature flags, and background jobs - posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) - Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/COMMANDMENTS.md new file mode 100644 index 00000000..9f024f48 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/COMMANDMENTS.md @@ -0,0 +1,23 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use posthog-rails gem alongside posthog-ruby for automatic exception capture and ActiveJob instrumentation +- Run `rails generate posthog:install` to create the initializer, or manually create config/initializers/posthog.rb +- Configure auto_capture_exceptions: true to automatically track unhandled exceptions in controllers +- Configure report_rescued_exceptions: true to also capture exceptions that Rails rescues (e.g. with rescue_from) +- Configure auto_instrument_active_job: true to track background job failures with job class, queue, and arguments +- Use PostHog.capture() and PostHog.identify() class-level methods (NOT instance methods) — the posthog-rails gem manages the client lifecycle via PostHog.init +- Do NOT manually create PostHog::Client instances in Rails — use PostHog.init in the initializer and PostHog.capture/identify everywhere else +- capture_exception takes POSITIONAL args: PostHog.capture_exception(exception, distinct_id, additional_properties) — do NOT use keyword args +- Define posthog_distinct_id on the User model for automatic user association in error reports — posthog-rails auto-detects by trying: posthog_distinct_id, distinct_id, id, pk, uuid (in order) +- For ActiveJob user association, use the class-level DSL `posthog_distinct_id ->(user) { user.email }` or pass user_id: in a hash argument +- Store the project token in Rails credentials or environment variables, never hardcode +- For frontend tracking alongside posthog-rails, add the posthog-js snippet to the layout template — posthog-js handles pageviews, session replay, and client-side errors while posthog-ruby handles backend events, server errors, feature flags, and background jobs +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/alerts.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/monitoring.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/ruby-on-rails.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/ruby-on-rails.md index de1d1560..74838eaf 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/ruby-on-rails.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/ruby-on-rails.md @@ -1,191 +1,609 @@ -# Ruby on Rails error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -1. 1 +# Ruby on Rails - Docs - ## Install the gems +Copy page - Required +# Ruby on Rails - Docs - Add the `posthog-ruby` and `posthog-rails` gems to your Gemfile: +PostHog makes it easy to get data about traffic and usage of your Ruby on Rails app. Integrating PostHog enables analytics, custom event capture, feature flags, and automatic exception tracking. - Gemfile +This guide walks you through integrating PostHog into your Rails app using the [posthog-rails gem](https://github.com/PostHog/posthog-ruby/tree/main/posthog-rails). - PostHog AI +## Beta: integration via LLM - ```ruby - gem "posthog-ruby" - gem "posthog-rails" - ``` +Install PostHog for Rails in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. - Then run: +`npx @posthog/wizard` - Terminal +[Learn more](/wizard.md) - PostHog AI +Or, to integrate manually, continue with the rest of this guide. - ```bash - bundle install - ``` +## Features -2. 2 +- **Automatic exception tracking** – Captures unhandled and rescued exceptions +- **ActiveJob instrumentation** – Tracks background job exceptions +- **User context** – Automatically associates exceptions with the current user +- **Smart filtering** – Excludes common Rails exceptions (404s, etc.) by default +- **Request context** – Adds request metadata and optional PostHog tracing header identity/session context to captured events +- **Rails 7.0+ error reporter** – Integrates with Rails' built-in error reporting +- **Log forwarding** – Optionally forwards `Rails.logger` output to [PostHog Logs](/docs/logs.md) over OpenTelemetry, automatically correlated with request context (Ruby 3.3+) - ## Generate the initializer +## Installation - Required +Add both gems to your Gemfile: - Run the install generator to create the PostHog initializer: +Gemfile - Terminal +PostHog AI - PostHog AI +```ruby +gem 'posthog-ruby', require: 'posthog' +gem 'posthog-rails' +``` - ```bash - rails generate posthog:install - ``` +Then run: - This will create `config/initializers/posthog.rb` with sensible defaults and documentation. +Terminal -3. 3 +PostHog AI - ## Configure PostHog +```bash +bundle install +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +### Generate the initializer + +Run the install generator to create the PostHog initializer: + +Terminal + +PostHog AI + +```bash +rails generate posthog:install +``` + +This creates `config/initializers/posthog.rb` with sensible defaults and documentation. + +## Configuration + +`PostHog.init` creates a single client instance used across your app. Avoid creating multiple `PostHog::Client` instances with the same API key, as this can cause dropped events and inconsistent behavior. + +The generated initializer includes the most common options: + +config/initializers/posthog.rb - Required +PostHog AI - Update `config/initializers/posthog.rb` with your project token and host: +```ruby +# Rails-specific configuration +PostHog::Rails.configure do |config| + config.auto_capture_exceptions = true # Enable automatic exception capture (default: false) + config.report_rescued_exceptions = true # Report exceptions Rails rescues (default: false) + config.auto_instrument_active_job = true # Instrument background jobs (default: false) + config.use_tracing_headers = true # Use PostHog tracing headers for identity/session context (default: true) + config.capture_user_context = true # Include authenticated user info in exceptions (default: true) + config.current_user_method = :current_user # Method to get current user (default: :current_user) + config.user_id_method = nil # Method to get ID from user object (default: auto-detect) + # Add additional exceptions to ignore + config.excluded_exceptions = ['MyCustomError'] +end +# Core PostHog client initialization +PostHog.init do |config| + # Required: Your PostHog project API key + config.api_key = '' + # Optional: Your PostHog instance URL + config.host = 'https://us.i.posthog.com' + # Optional: Personal API key for feature flags + config.personal_api_key = 'phx_xxxxxxxxx' + # Maximum number of events to queue before dropping (default: 10000) + config.max_queue_size = 10_000 + # Send events synchronously on the calling thread (default: false) + config.sync_mode = false + # Feature flags polling interval in seconds (default: 30) + config.feature_flags_polling_interval = 30 + # Feature flag request timeout in seconds (default: 3) + config.feature_flag_request_timeout_seconds = 3 + # Error callback to detect misconfiguration + config.on_error = proc { |status, msg| + Rails.logger.error("PostHog error: #{msg}") + } + # Before-send callback to modify or drop events + config.before_send = proc { |event| + event[:properties] ||= {} + event[:properties]['environment'] = Rails.env + event + } + # Disable network calls in test mode + config.test_mode = true if Rails.env.test? +end +``` - config/initializers/posthog.rb +You can find your project token and instance address in [your project settings](https://us.posthog.com/project/settings). - PostHog AI +> **Tip:** Use [`Rails.application.credentials`](https://guides.rubyonrails.org/security.html#custom-credentials) to avoid hardcoding API keys. First, add your keys and then reference them in your initializer: +> +> Terminal +> +> PostHog AI +> +> ```bash +> rails credentials:edit +> ``` +> +> config/credentials.yml.enc +> +> PostHog AI +> +> ```yaml +> posthog: +> api_key: +> host: https://us.i.posthog.com +> personal_api_key: phx_xxxxxxxxx +> ``` +> +> config/initializers/posthog.rb +> +> PostHog AI +> +> ```ruby +> config.api_key = Rails.application.credentials.posthog[:api_key] +> config.host = Rails.application.credentials.posthog[:host] +> config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +> ``` + +## Capturing events + +Track custom events anywhere in your Rails app: + +Ruby + +PostHog AI + +```ruby +PostHog.capture({ + distinct_id: current_user.id, + event: 'post_created', + properties: { title: @post.title } +}) +``` + +Identify a user and set their person properties: + +Ruby + +PostHog AI + +```ruby +PostHog.identify({ + distinct_id: current_user.id, + properties: { + email: current_user.email, + plan: current_user.plan + } +}) +``` + +The Rails integration delegates methods like `capture`, `identify`, `alias`, `group_identify`, `evaluate_flags`, `capture_exception`, `flush`, and `shutdown` to the initialized `PostHog::Client`. + +## Request context + +PostHog Rails automatically applies request-scoped context to events captured during web requests. Request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip` is added to event properties. + +When `use_tracing_headers` is enabled, PostHog tracing headers (`X-PostHog-Distinct-Id` and `X-PostHog-Session-Id`) are also used as default `distinct_id` and `$session_id` values. Explicit `distinct_id` and properties passed to `PostHog.capture` always take precedence. - ```ruby - PostHog.init do |config| - config.api_key = '' - config.host = 'https://us.i.posthog.com' - end - ``` +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Rails backend hostname so browser requests include the session and distinct ID headers. -4. 4 +Tracing headers are client-controlled analytics context, not authentication or authorization. Pass an authenticated `distinct_id` explicitly for security-sensitive server-side decisions. - ## Send events +Disable tracing header identity/session capture if you do not want client-supplied tracing headers used for server-side events. Request metadata is still captured: - Recommended +Ruby - Once installed, you can manually send events to test your integration: +PostHog AI - Ruby +```ruby +PostHog::Rails.config.use_tracing_headers = false +``` - PostHog AI +## Logs - ```ruby - PostHog.capture({ - distinct_id: 'user_123', - event: 'button_clicked', - properties: { - button_name: 'signup' - } - }) - ``` +To set up [PostHog Logs](/docs/logs.md) in your Rails app, follow the [Ruby on Rails logs installation guide](/docs/logs/installation/ruby-on-rails.md). The integration forwards `Rails.logger` output to PostHog Logs over OpenTelemetry, automatically correlated with each request's distinct ID and session ID. Requires Ruby 3.3+. -5. 5 +## Error tracking - ## Configure error tracking +For full details on setting up error tracking with Rails, see our [Rails error tracking installation guide](/docs/error-tracking/installation/ruby-on-rails.md). - Required +### Automatic exception tracking - Update `config/initializers/posthog.rb` to enable automatic exception capture: +When `auto_capture_exceptions` is enabled, exceptions are automatically captured: - config/initializers/posthog.rb +Ruby - PostHog AI +PostHog AI - ```ruby - PostHog::Rails.configure do |config| - config.auto_capture_exceptions = true - config.report_rescued_exceptions = true - config.auto_instrument_active_job = true - config.capture_user_context = true - config.current_user_method = :current_user - end - ``` +```ruby +class PostsController < ApplicationController + def show + @post = Post.find(params[:id]) + # Any exception here is automatically captured + end +end +``` -6. 6 +`report_rescued_exceptions` controls whether exceptions Rails rescues (for example, exceptions rendered by Rails error pages) are captured. Enable it along with `auto_capture_exceptions` for complete error visibility, or leave it disabled to capture only unhandled exceptions. - ## Automatic exception capture +### Manual exception capture - Recommended +You can also manually capture exceptions: - With `auto_capture_exceptions` enabled, exceptions are automatically captured from your controllers: +Ruby - app/controllers/posts\_controller.rb +PostHog AI - PostHog AI +```ruby +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' } +) +``` - ```ruby - class PostsController < ApplicationController - def show - @post = Post.find(params[:id]) - # Any exception here is automatically captured - end +If you evaluated feature flags for the request, pass the same snapshot to include matching flag properties on the exception event: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' }, + flags: flags +) +``` + +### Background job exceptions + +When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured with job context: + +Ruby + +PostHog AI + +```ruby +class EmailJob < ApplicationJob + def perform(user_id) + user = User.find(user_id) + UserMailer.welcome(user).deliver_now + # Exceptions are automatically captured + end +end +``` + +#### Associating jobs with users + +By default, PostHog extracts a `distinct_id` from job arguments by looking for a `user_id` key in hash arguments: + +Ruby + +PostHog AI + +```ruby +# PostHog will automatically use options[:user_id] as the distinct_id +ProcessOrderJob.perform_later(order.id, user_id: current_user.id) +``` + +For more control, use the `posthog_distinct_id` class method. The proc or block receives the same arguments as `perform`: + +Ruby + +PostHog AI + +```ruby +class SendWelcomeEmailJob < ApplicationJob + posthog_distinct_id ->(user, _options) { user.id } + def perform(user, options = {}) + UserMailer.welcome(user).deliver_now + end +end +``` + +You can also use a block: + +Ruby + +PostHog AI + +```ruby +class ProcessOrderJob < ApplicationJob + posthog_distinct_id do |_order, notify_user_id| + notify_user_id + end + def perform(order, notify_user_id) + # Process the order... + end +end +``` + +### Rails 7.0+ error reporter + +PostHog integrates with Rails' built-in error reporting: + +Ruby + +PostHog AI + +```ruby +# These errors are automatically sent to PostHog +Rails.error.handle do + # Code that might raise an error +end +Rails.error.record(exception, context: { user_id: current_user.id }) +``` + +PostHog automatically extracts the user's distinct ID from `user_id` or `distinct_id` in the context hash. Other context keys are included as properties on the exception event. + +### User context + +PostHog Rails automatically captures authenticated user information from your controllers for exceptions. Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity. + +If your user method has a different name, configure it: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.current_user_method = :logged_in_user +``` + +#### User ID extraction + +By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order: + +1. `posthog_distinct_id` – Define this on your User model for full control +2. `distinct_id` – Common analytics convention +3. `id` – Standard ActiveRecord primary key +4. `pk` – Primary key alias +5. `uuid` – For UUID-based primary keys + +It also checks hash-like users for `id`, `pk`, and `uuid` keys. + +You can configure a specific method: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.user_id_method = :email +``` + +Or define a method on your User model: + +Ruby + +PostHog AI + +```ruby +class User < ApplicationRecord + def posthog_distinct_id + "user_#{id}" # or external_id, or any unique identifier + end +end +``` + +### Excluded exceptions + +The following exceptions are not reported by default (common 4xx errors): + +- `AbstractController::ActionNotFound` +- `ActionController::BadRequest` +- `ActionController::InvalidAuthenticityToken` +- `ActionController::InvalidCrossOriginRequest` +- `ActionController::MethodNotAllowed` +- `ActionController::NotImplemented` +- `ActionController::ParameterMissing` +- `ActionController::RoutingError` +- `ActionController::UnknownFormat` +- `ActionController::UnknownHttpMethod` +- `ActionDispatch::Http::Parameters::ParseError` +- `ActiveRecord::RecordNotFound` +- `ActiveRecord::RecordNotUnique` + +Add more with: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.excluded_exceptions = ['MyException'] +``` + +## Feature flags + +Evaluate flags once for the current user, then read values from the returned snapshot: + +Ruby + +PostHog AI + +```ruby +class PostsController < ApplicationController + def show + flags = PostHog.evaluate_flags(current_user.id) + if flags.enabled?('new-post-design') + render 'posts/show_new' + else + render 'posts/show' end - ``` + end +end +``` + +For multivariate flags and experiments, use `get_flag`: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +variant = flags.get_flag('checkout-experiment') +if variant == 'test' + # Do something differently +end +``` + +When capturing an event after branching on a flag, pass the same `flags` snapshot so the event includes the exact flag values used by your code: -7. 7 +Ruby - ## Background jobs +PostHog AI - Optional +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture({ + distinct_id: current_user.id, + event: 'checkout_started', + flags: flags.only_accessed +}) +``` - When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured: +For local evaluation, ensure you've set `personal_api_key`: - app/jobs/email\_job.rb +Ruby + +PostHog AI + +```ruby +config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +``` + +See our [Ruby SDK docs](/docs/libraries/ruby.md#local-evaluation) for details on local evaluation with Puma and Unicorn servers. + +> **Note:** `PostHog.is_feature_enabled`, `PostHog.get_feature_flag`, `PostHog.get_feature_flag_result`, `PostHog.get_feature_flag_payload`, and `PostHog.capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `PostHog.evaluate_flags` for new code. + +## Testing + +In your test environment, disable network calls with test mode: + +config/environments/test.rb + +PostHog AI + +```ruby +PostHog.init do |config| + config.api_key = '' + config.test_mode = true +end +``` + +Or in your specs: + +spec/rails\_helper.rb + +PostHog AI + +```ruby +RSpec.configure do |config| + config.before(:each) do + allow(PostHog).to receive(:capture) + end +end +``` + +## Configuration reference + +### Core PostHog options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| api_key | String | required | Your PostHog project token. | +| host | String | https://us.i.posthog.com | Fully qualified PostHog API host. | +| personal_api_key | String | nil | Personal API key for local feature flag evaluation and remote config payloads. | +| max_queue_size | Integer | 10000 | Maximum number of events to keep in the async queue before dropping new events. | +| test_mode | Boolean | false | Keep events queued and do not send them. Useful for tests. | +| sync_mode | Boolean | false | Send events synchronously on the calling thread. | +| on_error | Proc | no-op | Callback called as on_error.call(status, error). | +| feature_flags_polling_interval | Integer | 30 | Seconds between local feature flag definition polls. | +| feature_flag_request_timeout_seconds | Integer | 3 | Timeout, in seconds, for feature flag requests. | +| before_send | Proc | nil | Callback that receives the event hash before it is queued or sent. Return a modified event hash, or nil to drop the event. | + +The `PostHog.init` block supports the options above. Less common core options like `batch_size`, `disable_singleton_warning`, `skip_ssl_verification`, and `flag_definition_cache_provider` can be passed as an options hash to `PostHog.init(...)`; see the [Ruby SDK docs](/docs/libraries/ruby.md#configuration) for details. + +### Rails-specific options + +Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| auto_capture_exceptions | Boolean | false | Automatically capture exceptions. | +| report_rescued_exceptions | Boolean | false | Report exceptions Rails rescues. | +| auto_instrument_active_job | Boolean | false | Capture ActiveJob exceptions with job context. | +| excluded_exceptions | Array | [] | Additional exception class names to ignore. | +| use_tracing_headers | Boolean | true | Use X-PostHog-Distinct-Id and X-PostHog-Session-Id as request-scoped defaults. | +| capture_user_context | Boolean | true | Include authenticated user info in exceptions. | +| current_user_method | Symbol | :current_user | Controller method used to fetch the current user. | +| user_id_method | Symbol | nil | Method used to extract the distinct ID from the user object. Auto-detects when nil. | + +## Troubleshooting + +### Exceptions not being captured + +1. Verify PostHog is initialized: + + Ruby PostHog AI ```ruby - class EmailJob < ApplicationJob - def perform(user_id) - user = User.find(user_id) - UserMailer.welcome(user).deliver_now - # Exceptions are automatically captured with job context - end - end + Rails.console + > PostHog.initialized? + => true ``` -8. 8 - - ## Manually capture exceptions - - Optional +2. Check your excluded exceptions list. - You can also manually capture exceptions that you handle in your application: +3. Verify middleware is installed: Ruby PostHog AI ```ruby - PostHog.capture_exception( - exception, - current_user.id, - { custom_property: 'value' } - ) + Rails.application.middleware ``` -9. ## Verify error tracking +### User context not working - Recommended +1. Verify `current_user_method` matches your controller method. +2. Check that the user object responds to `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid`. +3. If using a custom identifier, set `PostHog::Rails.config.user_id_method = :your_method`. - *Confirm events are being sent to PostHog* +### Feature flags not working - Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. +Ensure you've set `personal_api_key` in your configuration. - ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) +## Next steps - [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) +For any technical questions for how to integrate specific PostHog features into Rails (such as analytics, feature flags, A/B testing, etc.), have a look at our [Ruby SDK docs](/docs/libraries/ruby.md). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-ruby-on-rails/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/SKILL.md b/skills/posthog/all/skills/error-tracking-ruby/SKILL.md index 9a808d45..3c6e4731 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-ruby/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-ruby description: PostHog error tracking for Ruby metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Ruby @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Ruby applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) - Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs - In CLIs and scripts: MUST call client.shutdown before exit or all events are lost diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-ruby/references/COMMANDMENTS.md new file mode 100644 index 00000000..6652b15b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-ruby/references/COMMANDMENTS.md @@ -0,0 +1,11 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/alerts.md b/skills/posthog/all/skills/error-tracking-ruby/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-ruby/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-ruby/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/monitoring.md b/skills/posthog/all/skills/error-tracking-ruby/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/ruby.md b/skills/posthog/all/skills/error-tracking-ruby/references/ruby.md index 337af284..8cc10d01 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/ruby.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/ruby.md @@ -1,4 +1,10 @@ -# Ruby error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby Error Tracking installation - Docs + +Copy page + +# Ruby Error Tracking installation - Docs 1. 1 @@ -80,8 +86,8 @@ rescue => e posthog.capture_exception( e, - distinct_id: 'user_distinct_id', - properties: { + 'user_distinct_id', + { custom_property: 'custom_value' } ) @@ -94,7 +100,7 @@ | --- | --- | --- | | exception | Exception | The exception object to capture (required) | | distinct_id | String | The distinct ID of the user (optional) | - | properties | Hash | Additional properties to attach to the exception event (optional) | + | additional_properties | Hash | Additional properties to attach to the exception event (optional) | 5. ## Verify error tracking @@ -108,9 +114,9 @@ [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-ruby/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-ruby/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-ruby/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-ruby/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-rust/SKILL.md b/skills/posthog/all/skills/error-tracking-rust/SKILL.md new file mode 100644 index 00000000..db522be2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/SKILL.md @@ -0,0 +1,44 @@ +--- +name: error-tracking-rust +description: PostHog error tracking for Rust +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for Rust + +This skill helps you add PostHog error tracking to Rust applications. + +## Reference files + +- `references/rust.md` - Rust error tracking installation - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/error-tracking-rust/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-rust/references/COMMANDMENTS.md new file mode 100644 index 00000000..4eb31257 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/error-tracking-rust/references/alerts.md b/skills/posthog/all/skills/error-tracking-rust/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-rust/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-rust/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-rust/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-rust/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-rust/references/monitoring.md b/skills/posthog/all/skills/error-tracking-rust/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-rust/references/rust.md b/skills/posthog/all/skills/error-tracking-rust/references/rust.md new file mode 100644 index 00000000..3021282e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/rust.md @@ -0,0 +1,199 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Rust Error Tracking installation - Docs + +Copy page + +# Rust Error Tracking installation - Docs + +1. 1 + + ## Install the Rust SDK + + Required + + Install the [PostHog Rust SDK](/docs/libraries/rust.md): + + Terminal + + PostHog AI + + ```bash + cargo add posthog-rs + ``` + + Error tracking ships enabled by default through the `error-tracking` feature. If you build with `default-features = false`, add it back explicitly: + + toml + + PostHog AI + + ```toml + [dependencies] + posthog-rs = { version = "*", default-features = false, features = ["error-tracking"] } + ``` + + **Debug symbol uploads** + + The Rust SDK resolves stack traces in-process, so when the running binary carries debug info (as development builds do), captured frames include file names, line numbers, and function names without any symbol uploads. For resolved stack traces from release builds — which omit debug info by default — plus inlined frame resolution and source context (the surrounding lines of code in the error tracking UI), [upload debug symbols](/docs/error-tracking/upload-source-maps/rust.md). + +2. 2 + + ## Initialize the client + + Required + + Rust + + PostHog AI + + ```rust + let client = posthog_rs::client(( + "", + "https://us.i.posthog.com", + )).await; + ``` + + The default client is async (Tokio). Building with `default-features = false` gives you a blocking client instead — the same methods without `.await`. + +3. 3 + + ## Capture exceptions + + Required + + `capture_exception` works with any `std::error::Error` and captures it personlessly — the exception type, message, and full `source()` chain are sent, with a stack trace recorded at the call site: + + Rust + + PostHog AI + + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "connection refused"); + client.capture_exception(&error).await.unwrap(); + ``` + + To associate the exception with a person or attach context, use `capture_exception_with`: + + Rust + + PostHog AI + + ```rust + use posthog_rs::CaptureExceptionOptions; + client.capture_exception_with( + &error, + CaptureExceptionOptions::new() + .distinct_id("user_distinct_id") + .property("route", "/checkout").unwrap() + .group("company", "company_id") + .fingerprint("my-custom-fingerprint") + .level("warning"), + ).await.unwrap(); + ``` + + All options are optional: `distinct_id` links a person, `property` and `group` add context, `fingerprint` overrides [issue grouping](/docs/error-tracking/grouping-issues.md), and `level` sets the severity (defaults to `error`). + + If you use `anyhow`, pass the underlying error with `err.as_ref()`: + + Rust + + PostHog AI + + ```rust + let result: anyhow::Result<()> = do_work(); + if let Err(err) = result { + client.capture_exception(err.as_ref()).await.unwrap(); + } + ``` + +4. 4 + + ## Capture panics + + Optional + + Panic autocapture is opt-in and uses the process-global client. Enable `capture_panics` and initialize the global client with `init_global`; the SDK then installs a process-wide `std::panic` hook: + + Rust + + PostHog AI + + ```rust + use posthog_rs::{ClientOptionsBuilder, ErrorTrackingOptionsBuilder}; + let options = ClientOptionsBuilder::default() + .api_key("".to_string()) + .host("https://us.i.posthog.com") + .error_tracking( + ErrorTrackingOptionsBuilder::default() + .capture_panics(true) + .build() + .unwrap(), + ) + .build() + .unwrap(); + // Installs the panic hook and routes panics through the global client. + posthog_rs::init_global(options).await.unwrap(); + ``` + + Each panic is captured as a personless `$exception` carrying the panic message, the panic-site location, and a call-site stack trace (subject to `capture_stacktrace`). The previously installed hook still runs afterwards. + + Because a panic hook is process-global, panic autocapture pairs with the global client — there is no per-`Client` panic API. Capture routes through the SDK's background worker, so it needs no async runtime, and the flush is bounded to a short timeout (2s) so a slow or unreachable PostHog can't freeze the crashing process. Delivery is best-effort: under sustained backpressure the event may not be sent before the process exits. + +5. 5 + + ## Configure stack traces + + Optional + + Stack trace capture and in-app frame classification are configured per client through `ErrorTrackingOptionsBuilder`: + + Rust + + PostHog AI + + ```rust + use posthog_rs::{ClientOptionsBuilder, ErrorTrackingOptionsBuilder}; + let options = ClientOptionsBuilder::default() + .api_key("".to_string()) + .host("https://us.i.posthog.com") + .error_tracking( + ErrorTrackingOptionsBuilder::default() + // Skip the stack walk entirely, e.g. for high-volume handled errors + .capture_stacktrace(false) + // Mark frames from a crate as library code rather than in-app + .in_app_exclude_paths(vec!["other_crate::".to_string()]) + .build() + .unwrap(), + ) + .build() + .unwrap(); + let client = posthog_rs::client(options).await; + ``` + + In-app patterns match both file paths and function symbols, so crate prefixes like `"my_crate::"` and path fragments like `"/service/"` both work. By default, frames from the cargo registry, the standard library, and vendored or target paths are classified as library code. + +6. 6 + + ## Verify error tracking + + Recommended + + Trigger a test exception to confirm events are being sent to PostHog. You should see it appear in the [error tracking issues view](https://app.posthog.com/error_tracking). + + Rust + + PostHog AI + + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "This is a test exception from Rust"); + client.capture_exception(&error).await.unwrap(); + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-rust/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-rust/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-rust/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-svelte/SKILL.md b/skills/posthog/all/skills/error-tracking-svelte/SKILL.md index 930e3474..89cd38c0 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-svelte/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-svelte description: PostHog error tracking for Svelte metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Svelte @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Svelte applications. - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,5 +32,10 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-svelte/references/COMMANDMENTS.md new file mode 100644 index 00000000..a5053c3d --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-svelte/references/COMMANDMENTS.md @@ -0,0 +1,11 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/alerts.md b/skills/posthog/all/skills/error-tracking-svelte/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-svelte/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-svelte/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/monitoring.md b/skills/posthog/all/skills/error-tracking-svelte/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/svelte.md b/skills/posthog/all/skills/error-tracking-svelte/references/svelte.md index cf1cb549..3a32d170 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/svelte.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/svelte.md @@ -1,4 +1,10 @@ -# SvelteKit error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# SvelteKit Error Tracking installation - Docs + +Copy page + +# SvelteKit Error Tracking installation - Docs 1. 1 @@ -50,7 +56,7 @@ '', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30' } ) } @@ -210,9 +216,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/web.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-svelte/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-svelte/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-svelte/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-svelte/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-android/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/SKILL.md new file mode 100644 index 00000000..6dc04b14 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/SKILL.md @@ -0,0 +1,409 @@ +--- +name: error-tracking-upload-source-maps-android +description: Upload ProGuard / R8 mapping files to PostHog Error Tracking for Android +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Android + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/android.md` - Upload mappings for android - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Android. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Android reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Android are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/COMMANDMENTS.md new file mode 100644 index 00000000..c18de5d4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/COMMANDMENTS.md @@ -0,0 +1,9 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/android.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/android.md new file mode 100644 index 00000000..83a0e9d0 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/android.md @@ -0,0 +1,183 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload mappings for Android - Docs + +Copy page + +# Upload mappings for Android - Docs + +1. 1 + + ## Download CLI + + Required + + > [CLI v0.7.4](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.4) or later + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Inject and upload + + Required + + > [AGP v8](https://developer.android.com/build/releases/gradle-plugin) or later + + Automatic mappings uploading is handled through the Gradle build process on Android. + + Install the [PostHog Android Gradle Plugin](https://github.com/PostHog/posthog-android/blob/main/posthog-android-gradle-plugin/CHANGELOG.md) on your app's `build.gradle.kts` file. + + Kotlin + + PostHog AI + + ```kotlin + // Available through mavenCentral() + plugins { + id("com.android.application") + id("com.posthog.android") version "$version" + ... + } + ``` + + If you are running this in CI/CD, you can configure the CLI directly on the Gradle task instead of relying on `POSTHOG_CLI_HOST`, `POSTHOG_CLI_PROJECT_ID`, and `POSTHOG_CLI_API_KEY` environment variables: + + Kotlin + + PostHog AI + + ```kotlin + import com.posthog.android.PostHogCliExecTask + tasks.withType { + postHogHost = "https://eu.posthog.com" + postHogProjectId = "my-project-id" + postHogApiKey = "my-personal-api-key" + } + ``` + + You can also set `postHogExecutable` if you want to use a custom `posthog-cli` path. + +4. 4 + + ## Upload native debug symbols (NDK) + + Optional + + > Requires [PostHog Android SDK 3.60.0](https://github.com/PostHog/posthog-android/releases/tag/android-v3.60.0) or later with `errorTrackingConfig.captureNativeCrashes` enabled, [Gradle plugin 1.5.0](https://github.com/PostHog/posthog-android/releases/tag/androidPlugin-v1.5.0) or later, and [CLI 0.7.32](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.32) or later. + + If your app includes native C or C++ code built with the NDK, upload the `.so` debug symbols so native crash stack traces resolve to function names, files, and line numbers. + + Enable the upload in your app's `build.gradle.kts`: + + Kotlin + + PostHog AI + + ```kotlin + posthog { + uploadNativeSymbols.set(true) + } + ``` + + When enabled, the Gradle plugin's `uploadPostHogNativeSymbols` task reads the variant's unstripped native libraries and uploads every one carrying debug info and a build ID, automatically after `assemble`, `install`, or `bundle`. This covers native code you build with the NDK as well as `.so` files bundled from `jniLibs` or dependencies, for minified and non-minified builds alike. Debuggable variants are skipped, so day-to-day debug builds don't upload symbols. + + To include source context around resolved crash frames, also bundle the project sources referenced by the debug info: + + Kotlin + + PostHog AI + + ```kotlin + posthog { + uploadNativeSymbols.set(true) + includeNativeSymbolSources.set(true) + } + ``` + + You can also run the upload task explicitly, for any variant, without enabling the automatic upload: + + Terminal + + PostHog AI + + ```bash + ./gradlew uploadPostHogNativeSymbolsRelease + ``` + + You can also upload a directory of `.so` files directly, without the Gradle plugin: + + Terminal + + PostHog AI + + ```bash + posthog-cli symbol-sets upload --directory app/build/intermediates/merged_native_libs/release + ``` + + PostHog matches crash frames to uploaded symbols by each library's GNU build ID, which the NDK emits by default. Each build has its own build ID, so symbols must be re-uploaded for every build you ship. + + Native crash events are captured on the next app launch, so event properties like `$app_version` reflect the app at capture time, not at crash time. If the app updated in between, check the crash's release (matched by build ID) rather than the event's version properties when investigating. + +6. ## Verify mappings upload + + Checkpoint + + Confirm that mappings are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-android/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/SKILL.md new file mode 100644 index 00000000..c6119820 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/SKILL.md @@ -0,0 +1,412 @@ +--- +name: error-tracking-upload-source-maps-angular +description: Upload source maps to PostHog Error Tracking for Angular +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Angular + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/angular.md` - Upload source maps for angular - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Angular. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Angular reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Angular are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. +- Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. +- Always use standalone components over NgModules. +- Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/COMMANDMENTS.md new file mode 100644 index 00000000..3876eeda --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/COMMANDMENTS.md @@ -0,0 +1,12 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. +- Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. +- Always use standalone components over NgModules. +- Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/angular.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/angular.md new file mode 100644 index 00000000..e5e7e76e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/angular.md @@ -0,0 +1,204 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Angular - Docs + +Copy page + +# Upload source maps for Angular - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Output source maps for Angular + + Required + + You can configure Angular to [generate source maps](https://angular.dev/reference/configs/workspace-config#source-map-configuration) by adding the following to your `angular.json` file: + + angular.json + + PostHog AI + + ```json + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "sourceMap": { + "scripts": true, + "styles": true, + "hidden": true, + "vendor": true + } + } + } + ``` + + Then, build your Angular application: + + Terminal + + PostHog AI + + ```bash + ng build --configuration production + ``` + +4. ## Verify source map generation + + Checkpoint + + *Confirm source maps are generated* + + Confirm that the source maps are generated in the `dist//` directory. You should see a `.js.map` file for each JS bundle. + +5. 4 + + ## Inject source map + + Required + + *Your goal in this step: Add metadata to associate maps with your code.* + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + +6. ## Verify source map injection + + Checkpoint + + *Confirm source map comments are present* + + Confirm that the served files are injected with the correct source map comment in production in dev tools: + + You should see a comment like this in your minified JS files, for example `main-IX6K2QJM.js`: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +7. 5 + + ## Upload source map + + Required + + *Your goal in this step: Send the processed source maps to PostHog.* + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + + #### Serve injected assets + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. We suggest you upload source maps right after your production build in CI. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +8. ## Verify source map upload + + Checkpoint + + *Confirm source maps are successfully uploaded* + + Before proceeding, confirm that source maps are successfully uploaded to PostHog. + + [Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-angular/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/SKILL.md new file mode 100644 index 00000000..665a6f8a --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/SKILL.md @@ -0,0 +1,417 @@ +--- +name: error-tracking-upload-source-maps-flutter +description: Upload Flutter debug symbols to PostHog Error Tracking +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Flutter + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/flutter.md` - Upload debug symbols for flutter - docs +- `references/web.md` - Upload source maps for web - docs +- `references/android.md` - Upload mappings for android - docs +- `references/ios.md` - Upload dsyms for ios - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Flutter. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Flutter reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Flutter are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/COMMANDMENTS.md new file mode 100644 index 00000000..58624ee9 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/android.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/android.md new file mode 100644 index 00000000..83a0e9d0 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/android.md @@ -0,0 +1,183 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload mappings for Android - Docs + +Copy page + +# Upload mappings for Android - Docs + +1. 1 + + ## Download CLI + + Required + + > [CLI v0.7.4](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.4) or later + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Inject and upload + + Required + + > [AGP v8](https://developer.android.com/build/releases/gradle-plugin) or later + + Automatic mappings uploading is handled through the Gradle build process on Android. + + Install the [PostHog Android Gradle Plugin](https://github.com/PostHog/posthog-android/blob/main/posthog-android-gradle-plugin/CHANGELOG.md) on your app's `build.gradle.kts` file. + + Kotlin + + PostHog AI + + ```kotlin + // Available through mavenCentral() + plugins { + id("com.android.application") + id("com.posthog.android") version "$version" + ... + } + ``` + + If you are running this in CI/CD, you can configure the CLI directly on the Gradle task instead of relying on `POSTHOG_CLI_HOST`, `POSTHOG_CLI_PROJECT_ID`, and `POSTHOG_CLI_API_KEY` environment variables: + + Kotlin + + PostHog AI + + ```kotlin + import com.posthog.android.PostHogCliExecTask + tasks.withType { + postHogHost = "https://eu.posthog.com" + postHogProjectId = "my-project-id" + postHogApiKey = "my-personal-api-key" + } + ``` + + You can also set `postHogExecutable` if you want to use a custom `posthog-cli` path. + +4. 4 + + ## Upload native debug symbols (NDK) + + Optional + + > Requires [PostHog Android SDK 3.60.0](https://github.com/PostHog/posthog-android/releases/tag/android-v3.60.0) or later with `errorTrackingConfig.captureNativeCrashes` enabled, [Gradle plugin 1.5.0](https://github.com/PostHog/posthog-android/releases/tag/androidPlugin-v1.5.0) or later, and [CLI 0.7.32](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.32) or later. + + If your app includes native C or C++ code built with the NDK, upload the `.so` debug symbols so native crash stack traces resolve to function names, files, and line numbers. + + Enable the upload in your app's `build.gradle.kts`: + + Kotlin + + PostHog AI + + ```kotlin + posthog { + uploadNativeSymbols.set(true) + } + ``` + + When enabled, the Gradle plugin's `uploadPostHogNativeSymbols` task reads the variant's unstripped native libraries and uploads every one carrying debug info and a build ID, automatically after `assemble`, `install`, or `bundle`. This covers native code you build with the NDK as well as `.so` files bundled from `jniLibs` or dependencies, for minified and non-minified builds alike. Debuggable variants are skipped, so day-to-day debug builds don't upload symbols. + + To include source context around resolved crash frames, also bundle the project sources referenced by the debug info: + + Kotlin + + PostHog AI + + ```kotlin + posthog { + uploadNativeSymbols.set(true) + includeNativeSymbolSources.set(true) + } + ``` + + You can also run the upload task explicitly, for any variant, without enabling the automatic upload: + + Terminal + + PostHog AI + + ```bash + ./gradlew uploadPostHogNativeSymbolsRelease + ``` + + You can also upload a directory of `.so` files directly, without the Gradle plugin: + + Terminal + + PostHog AI + + ```bash + posthog-cli symbol-sets upload --directory app/build/intermediates/merged_native_libs/release + ``` + + PostHog matches crash frames to uploaded symbols by each library's GNU build ID, which the NDK emits by default. Each build has its own build ID, so symbols must be re-uploaded for every build you ship. + + Native crash events are captured on the next app launch, so event properties like `$app_version` reflect the app at capture time, not at crash time. If the app updated in between, check the crash's release (matched by build ID) rather than the event's version properties when investigating. + +6. ## Verify mappings upload + + Checkpoint + + Confirm that mappings are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/flutter.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/flutter.md new file mode 100644 index 00000000..119815e8 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/flutter.md @@ -0,0 +1,48 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload debug symbols for Flutter - Docs + +Copy page + +# Upload debug symbols for Flutter - Docs + +**CLI version requirement** + +A minimum CLI version of [0.7.4](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.4) is required, but we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md) to ensure you have all of error tracking's features. + +Flutter apps can run on multiple platforms, each with its own debug symbol format. Follow the relevant section below for each platform you support. + +## Flutter Web + +Ensure source maps are generated: + +Terminal + +PostHog AI + +```bash +# Files are usually under the 'build/web' folder +flutter build web --source-maps +``` + +Then use our standard [CLI approach](/docs/error-tracking/upload-source-maps/cli.md) to inject and upload them. + +## iOS / macOS + +When `captureNativeExceptions` is enabled, native crashes on Apple platforms are captured automatically. To get symbolicated stack traces, you need to upload dSYM debug symbols. + +Follow the [iOS source maps guide](/docs/error-tracking/upload-source-maps/ios.md) to set up a build phase script in Xcode that uploads dSYMs automatically. + +## Android + +When `captureNativeExceptions` is enabled, Java/Kotlin exceptions on Android are captured automatically. If your app uses ProGuard or R8 minification, you need to upload mapping files so stack traces can be deobfuscated. + +Follow the [Android mappings guide](/docs/error-tracking/upload-mappings/android.md) to set up the PostHog Gradle plugin for automatic uploads. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/ios.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/ios.md new file mode 100644 index 00000000..b2575be4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/ios.md @@ -0,0 +1,286 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload dSYMs for iOS - Docs + +Copy page + +# Upload dSYMs for iOS - Docs + +**CLI version requirement** + +A minimum CLI version of [0.7.7](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.7) is required, but we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md) to ensure you have all of Error Tracking's features. + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Configure build settings + + Required + + In Xcode, configure your build settings to generate dSYMs: + + 1. Open your project in Xcode + 2. Select your target + 3. Go to **Build Settings** + 4. Search for **Debug Information Format** + 5. Make sure Release configurations have **DWARF with dSYM File** + + ![Xcode dSYM setting](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/DWARF_and_d_SYM_xcode_settings_light_f6a8bb74c3.png)![Xcode dSYM setting](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/DWARF_and_d_SYM_xcode_settings_7b2c628b52.png) + + **Disable User Script Sandboxing** + + You must disable User Script Sandboxing for the upload script to work: + + 1. In Build Settings, search for **User Script Sandboxing**(`ENABLE_USER_SCRIPT_SANDBOXING`) + 2. Set `ENABLE_USER_SCRIPT_SANDBOXING` to **No** + + **Why is this required?** + + When User Script Sandboxing is enabled, Xcode only allows run scripts to access files explicitly specified in the build phase's **Input Files**. The dSYM upload script needs to walk directories to locate and read dSYM bundles, and execute `posthog-cli` which are currently not allowed with User Script Sandboxing enabled. + +4. 4 + + ## Add build phase script + + Required + + To symbolicate crash reports, PostHog needs your project's debug symbol (dSYM) files. The following script automatically processes and uploads dSYMs whenever you build your app. + + Add a **Run Script** build phase: + + 1. In Xcode, select your main app target + 2. Go to **Build Phases** tab + 3. Click the **+** button and select **New Run Script Phase** + 4. Make sure it's set to run last (after "Copy Bundle Resources" or similar) + 5. Add the appropriate script for your package manager: + + PostHog AI + + ### Swift + + ```bash + ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + 6. In the **Input Files** section of the Run Script phase, add: + + PostHog AI + + ``` + $(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME) + ``` + + This makes Xcode wait for the app's dSYM contents before running the upload script. + +5. 5 + + ## Optional: Include source code context + + Optional + + By default, only debug symbols are uploaded. To include source code snippets in your stack traces (for better debugging context), set the `POSTHOG_INCLUDE_SOURCE` environment variable: + + 1. In the Run Script build phase, click the chevron to expand + 2. Set the **environment Variable** when calling the upload script: + + PostHog AI + + ### Swift + + ```bash + POSTHOG_INCLUDE_SOURCE=1 ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + POSTHOG_INCLUDE_SOURCE=1 ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + **Note**: This increases upload size and build times. Only enable if you need source code context in PostHog's error tracking UI. + +6. 6 + + ## Optional: Skip conflicting uploads + + Optional + + If PostHog already has a dSYM with the same UUID but different content, the upload fails with `content_hash_mismatch` and stops your build. To skip these conflicting uploads and keep the existing symbols instead, set the `POSTHOG_SKIP_ON_CONFLICT` environment variable when calling the upload script: + + PostHog AI + + ### Swift + + ```bash + POSTHOG_SKIP_ON_CONFLICT=1 ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + POSTHOG_SKIP_ON_CONFLICT=1 ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + Requires posthog-ios 3.64.7 or later and posthog-cli 0.7.12 or later. + +7. 7 + + ## Build and verify + + Required + + Build your app in Xcode. The dSYM upload script will automatically run and upload symbols to PostHog. + + Check the build log output for confirmation. + +8. 8 + + ## Force a test crash + + Optional + + To verify everything is working end-to-end, force a test crash and confirm it appears in PostHog. + + 1. Add a crash trigger button to your app: + + PostHog AI + + ### SwiftUI + + ```swift + Button("Test Crash") { + fatalError("PostHog test crash") + } + ``` + + ### UIKit + + ```swift + let button = UIButton(type: .system) + button.setTitle("Test Crash", for: .normal) + button.addTarget(self, action: #selector(testCrash), for: .touchUpInside) + view.addSubview(button) + @objc func testCrash() { + fatalError("PostHog test crash") + } + ``` + + 2. Build and run your app in Xcode, + + - Make sure the debugger is not attached, since it will intercept the crash and prevent the report from being collected. You can: + - Detach the debugger (click the stop button in Xcode) and run the app directly from the device/simulator home screen + - Or uncheck "Debug executable" in the scheme settings before running + 3. Reopen the app from your device or simulator's home screen (not from Xcode). + + 4. Tap the **Test Crash** button. The app will crash. The exception is collected but **not yet sent** to PostHog. + + 5. Reopen the app once more. This triggers the SDK to send the previously collected crash report to PostHog. + + 6. Check the [error tracking dashboard](https://app.posthog.com/error_tracking) for your test crash. + + Remove the crash trigger button before shipping to production. + +10. ## Verify dSYMs upload + + Checkpoint + + Confirm that dSYMs are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +## Troubleshooting + +### Script fails with "error: posthog-cli not found" + +Install the CLI using one of these methods: + +Terminal + +PostHog AI + +```bash +# NPM global install +npm install -g @posthog/cli +# Or download binary +curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh +``` + +### Script fails with permission errors + +Ensure `ENABLE_USER_SCRIPT_SANDBOXING` is set to `NO` in your build settings. + +### dSYMs not being generated + +Verify that `DEBUG_INFORMATION_FORMAT` is set to **DWARF with dSYM File** for the configuration you're building (Debug or Release). + +### Script fails with "content\_hash\_mismatch" + +This happens when the upload script runs before Xcode finishes generating the dSYM files. Leave **Based on dependency analysis** enabled and add `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` to the **Input Files** of the Run Script build phase. This makes Xcode wait for the app's dSYM contents before running the upload script. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file From 4812ca452e63d40d00d33af43029b041bcdde7bc Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:30:18 -0400 Subject: [PATCH 03/13] Update generated plugins from context-mill (mirror test) --- .../references/web.md | 170 + .../SKILL.md | 415 ++ .../references/COMMANDMENTS.md | 15 + .../references/cli.md | 150 + .../references/go.md | 208 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 420 ++ .../references/COMMANDMENTS.md | 20 + .../references/cli.md | 150 + .../references/ios.md | 286 ++ .../references/upload-source-maps.md | 67 + .../SKILL.md | 417 ++ .../references/COMMANDMENTS.md | 17 + .../references/cli.md | 150 + .../references/nextjs.md | 113 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 415 ++ .../references/COMMANDMENTS.md | 15 + .../references/cli.md | 150 + .../references/node.md | 166 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 409 ++ .../references/COMMANDMENTS.md | 8 + .../references/cli.md | 150 + .../references/nuxt-3-7.md | 187 + .../references/nuxt.md | 164 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 412 ++ .../references/COMMANDMENTS.md | 12 + .../references/cli.md | 150 + .../references/react-native.md | 452 +++ .../references/upload-source-maps.md | 67 + .../SKILL.md | 416 ++ .../references/COMMANDMENTS.md | 16 + .../references/cli.md | 150 + .../references/react.md | 175 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 408 ++ .../references/COMMANDMENTS.md | 8 + .../references/cli.md | 150 + .../references/rollup.md | 120 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 414 ++ .../references/COMMANDMENTS.md | 14 + .../references/cli.md | 150 + .../references/rust.md | 223 + .../references/upload-source-maps.md | 67 + .../SKILL.md | 408 ++ .../references/COMMANDMENTS.md | 8 + .../references/cli.md | 150 + .../references/upload-source-maps.md | 67 + .../references/vite.md | 103 + .../SKILL.md | 419 ++ .../references/COMMANDMENTS.md | 19 + .../references/cli.md | 150 + .../references/upload-source-maps.md | 67 + .../references/web.md | 170 + .../SKILL.md | 408 ++ .../references/COMMANDMENTS.md | 8 + .../references/cli.md | 150 + .../references/upload-source-maps.md | 67 + .../references/webpack.md | 99 + .../all/skills/error-tracking-web/SKILL.md | 15 +- .../references/COMMANDMENTS.md | 19 + .../error-tracking-web/references/alerts.md | 16 +- .../references/assigning-issues.md | 50 +- .../references/fingerprints.md | 10 +- .../references/monitoring.md | 18 +- .../references/upload-source-maps.md | 28 +- .../error-tracking-web/references/web.md | 18 +- .../skills/error-tracking-wordpress/SKILL.md | 50 + .../references/COMMANDMENTS.md | 19 + .../references/alerts.md | 69 + .../references/assigning-issues.md | 103 + .../references/fingerprints.md | 63 + .../references/monitoring.md | 146 + .../references/php.md | 228 ++ .../references/upload-source-maps.md | 67 + .../references/wordpress.md | 109 + .../all/skills/feature-flags-android/SKILL.md | 6 +- .../references/COMMANDMENTS.md | 9 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/android.md | 16 +- .../references/best-practices.md | 238 +- .../all/skills/feature-flags-api/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 5 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../feature-flags-api/references/api.md | 12 +- .../references/best-practices.md | 238 +- .../SKILL.md | 48 + .../references/COMMANDMENTS.md | 15 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../references/dotnet.md | 773 ++++ .../all/skills/feature-flags-django/SKILL.md | 51 + .../references/COMMANDMENTS.md | 20 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../feature-flags-django/references/django.md | 300 ++ .../feature-flags-django/references/python.md | 197 + .../all/skills/feature-flags-dotnet/SKILL.md | 13 +- .../references/COMMANDMENTS.md | 10 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-dotnet/references/dotnet.md | 548 ++- .../all/skills/feature-flags-elixir/SKILL.md | 18 +- .../references/COMMANDMENTS.md | 16 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-elixir/references/elixir.md | 16 +- .../all/skills/feature-flags-flask/SKILL.md | 49 + .../references/COMMANDMENTS.md | 18 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../feature-flags-flask/references/flask.md | 147 + .../feature-flags-flask/references/python.md | 197 + .../all/skills/feature-flags-flutter/SKILL.md | 16 +- .../references/COMMANDMENTS.md | 14 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../references/flutter.md | 28 +- .../all/skills/feature-flags-go/SKILL.md | 17 +- .../references/COMMANDMENTS.md | 15 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../skills/feature-flags-go/references/go.md | 12 +- .../all/skills/feature-flags-ios/SKILL.md | 22 +- .../references/COMMANDMENTS.md | 20 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-ios/references/ios.md | 22 +- .../feature-flags-ios/references/usage.md | 641 +++ .../all/skills/feature-flags-java/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 5 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-java/references/java.md | 12 +- .../all/skills/feature-flags-laravel/SKILL.md | 46 + .../references/COMMANDMENTS.md | 15 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../references/laravel.md | 176 + .../feature-flags-laravel/references/php.md | 193 + .../all/skills/feature-flags-nextjs/SKILL.md | 10 +- .../references/COMMANDMENTS.md | 26 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../references/next-js.md | 118 +- .../feature-flags-nextjs/references/react.md | 24 +- .../all/skills/feature-flags-nodejs/SKILL.md | 10 +- .../references/COMMANDMENTS.md | 8 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-nodejs/references/nodejs.md | 12 +- .../all/skills/feature-flags-php/SKILL.md | 7 +- .../references/COMMANDMENTS.md | 11 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-php/references/php.md | 12 +- .../all/skills/feature-flags-python/SKILL.md | 6 +- .../references/COMMANDMENTS.md | 15 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-python/references/python.md | 14 +- .../feature-flags-react-native/SKILL.md | 9 +- .../references/COMMANDMENTS.md | 20 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../references/react-native.md | 16 +- .../all/skills/feature-flags-react/SKILL.md | 9 +- .../references/COMMANDMENTS.md | 16 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-react/references/react.md | 24 +- .../feature-flags-ruby-on-rails/SKILL.md | 54 + .../references/COMMANDMENTS.md | 23 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../references/ruby-on-rails.md | 610 +++ .../references/ruby.md | 206 + .../all/skills/feature-flags-ruby/SKILL.md | 6 +- .../references/COMMANDMENTS.md | 11 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-ruby/references/ruby.md | 12 +- .../all/skills/feature-flags-rust/SKILL.md | 16 +- .../references/COMMANDMENTS.md | 14 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-rust/references/rust.md | 194 +- .../all/skills/feature-flags-web/SKILL.md | 10 +- .../references/COMMANDMENTS.md | 8 + .../references/adding-feature-flag-code.md | 1821 +++++---- .../references/best-practices.md | 238 +- .../feature-flags-web/references/web.md | 29 +- .../skills/feature-flags-wordpress/SKILL.md | 50 + .../references/COMMANDMENTS.md | 19 + .../references/adding-feature-flag-code.md | 3584 +++++++++++++++++ .../references/best-practices.md | 237 ++ .../feature-flags-wordpress/references/php.md | 193 + 200 files changed, 58756 insertions(+), 16136 deletions(-) create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/web.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-go/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/go.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-ios/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/ios.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/nextjs.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-node/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/node.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt-3-7.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/react-native.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/react.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/rollup.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rust/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/rust.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-vite/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/vite.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-web/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/web.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/cli.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/webpack.md create mode 100644 skills/posthog/all/skills/error-tracking-web/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/SKILL.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/alerts.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/assigning-issues.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/fingerprints.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/monitoring.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/php.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/upload-source-maps.md create mode 100644 skills/posthog/all/skills/error-tracking-wordpress/references/wordpress.md create mode 100644 skills/posthog/all/skills/feature-flags-android/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-api/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/dotnet.md create mode 100644 skills/posthog/all/skills/feature-flags-django/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-django/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-django/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-django/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-django/references/django.md create mode 100644 skills/posthog/all/skills/feature-flags-django/references/python.md create mode 100644 skills/posthog/all/skills/feature-flags-dotnet/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-elixir/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/references/flask.md create mode 100644 skills/posthog/all/skills/feature-flags-flask/references/python.md create mode 100644 skills/posthog/all/skills/feature-flags-flutter/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-go/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-ios/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-ios/references/usage.md create mode 100644 skills/posthog/all/skills/feature-flags-java/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/references/laravel.md create mode 100644 skills/posthog/all/skills/feature-flags-laravel/references/php.md create mode 100644 skills/posthog/all/skills/feature-flags-nextjs/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-nodejs/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-php/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-python/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-react-native/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-react/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby-on-rails.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby.md create mode 100644 skills/posthog/all/skills/feature-flags-ruby/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-rust/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-web/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/SKILL.md create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/references/adding-feature-flag-code.md create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/references/best-practices.md create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/references/php.md diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/web.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/web.md new file mode 100644 index 00000000..96b04427 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-flutter/references/web.md @@ -0,0 +1,170 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for web - Docs + +Copy page + +# Upload source maps for web - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Output source maps for web + + Required + + If you serve minified bundles in production, PostHog requires source maps to generate accurate stack traces. Here are instructions to enable source map generation for popular build tools: + + | Build Tool | Documentation | + | --- | --- | + | Vite | [Source Map Configuration](https://v3.vitejs.dev/config/build-options.html#build-sourcemap) | + | webpack | [Source Map Configuration](https://webpack.js.org/configuration/devtool/) | + | Rollup | [Source Map Options](https://rollupjs.org/configuration-options/#output-sourcemap) | + + For other build tools, consult their documentation to enable source maps. + +4. 4 + + ## Inject source map + + Required + + *Your goal in this step: Add metadata to associate maps with your code.* + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + +5. ## Verify source map injection + + Checkpoint + + *Confirm source map comments are present* + + Confirm that the served files are injected with the correct source map comment in production in dev tools. + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +6. 5 + + ## Upload source map + + Required + + *Your goal in this step: Send the processed source maps to PostHog.* + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + + #### Serve injected assets + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. We suggest you upload source maps right after your production build in CI. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +8. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-go/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/SKILL.md new file mode 100644 index 00000000..b589e41b --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/SKILL.md @@ -0,0 +1,415 @@ +--- +name: error-tracking-upload-source-maps-go +description: Upload native debug symbols to PostHog Error Tracking for Go +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Go + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/go.md` - Upload debug symbols for go - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Go. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Go reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Go are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/COMMANDMENTS.md new file mode 100644 index 00000000..68b721fe --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/go.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/go.md new file mode 100644 index 00000000..08618faf --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/go.md @@ -0,0 +1,208 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload debug symbols for Go - Docs + +Copy page + +# Upload debug symbols for Go - Docs + +The [Go SDK](/docs/error-tracking/installation/go.md) resolves stack traces in-process from the Go runtime, so captured frames already carry file names, line numbers, function names, and inlined calls. What they can't carry is your source code. + +That's what uploading debug symbols adds. PostHog resolves the frames again against the exact build that crashed, so the UI can show the lines of code around each frame. It also classifies frames as app or library code from their real source paths, instead of guessing from the function name. + +**Version requirements** + +- [posthog-go 1.22.0](https://github.com/PostHog/posthog-go/releases/tag/v1.22.0) or later, which records the instruction addresses and binary identity server-side symbolication needs. We recommend the latest version. +- [CLI 0.9.1](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.9.1) or later, which uploads macOS Go binaries directly and bundles Go project sources with `--include-source`. Linux binaries without source context work with CLI 0.7.32 or later. As always, we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md). + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Build with the right linker flags + + Required + + Go embeds DWARF debug info in every binary by default. What you add on top depends on the platform. + + On Linux, add a GNU build ID. That's the identifier PostHog uses to match stack frames to uploaded symbols, and Go doesn't emit one unless you ask: + + Terminal + + PostHog AI + + ```bash + go build -ldflags="-B gobuildid" -o my-app . + ``` + + On macOS there's no build ID step, since every Mach-O binary carries a UUID. Instead, turn off DWARF compression. Go compresses debug info by default, and symbolication can't read the compressed form yet: + + Terminal + + PostHog AI + + ```bash + go build -ldflags="-compressdwarf=false" -o my-app . + ``` + + Two flags to avoid: `-ldflags="-w"` and `-ldflags="-s"` strip the DWARF entirely, and `-trimpath` rewrites the source paths that `--include-source` reads from. + +4. 4 + + ## Build and upload + + Required + + Point the CLI at the directory containing your built binary: + + Terminal + + PostHog AI + + ```bash + posthog-cli symbol-sets upload --directory ./bin + ``` + + The CLI scans the directory and uploads every executable that carries debug info and an identity, skipping everything else. On macOS the Go binary uploads directly. Go never produces a `.dSYM` bundle, and it doesn't need one. Universal (fat) binaries upload one symbol set per architecture slice. Windows isn't supported yet, so the SDK falls back to plain runtime-resolved frames there. + + Run this in the same pipeline that produces your production binary. Every build gets its own identity, so you need to re-upload for each build you deploy. + +5. 5 + + ## Optional: Include source code context + + Optional + + By default, only debug symbols are uploaded. To also display the source code around each frame in your stack traces, add `--include-source`: + + Terminal + + PostHog AI + + ```bash + posthog-cli symbol-sets upload --directory ./bin --include-source + ``` + + This bundles the project source files referenced by the debug info into the upload. Uploads get bigger, so turn it on only if you want source context in the error tracking UI. It also means building without `-trimpath`, since the CLI reads the sources off disk using the paths recorded at compile time. + +6. 6 + + ## Optional: Upload from CI + + Optional + + In CI, authenticate with environment variables instead of `posthog-cli login`. For GitHub Actions: + + YAML + + PostHog AI + + ```yaml + - name: Build release binary + run: go build -ldflags="-B gobuildid" -o bin/my-app . + - name: Upload debug symbols + run: posthog-cli symbol-sets upload --directory ./bin + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + ``` + + Scope the credentials to the upload step. The build itself doesn't need them. + + The CLI defaults to US Cloud. If you're on EU Cloud or self-hosted, also set `POSTHOG_CLI_HOST` (for example `https://eu.posthog.com`). + + If your binary is built inside a Dockerfile, run the upload in the build stage right after the build, and pass the credentials in as build secrets so they never reach the runtime image. + +8. ## Verify debug symbols upload + + Checkpoint + + Confirm that debug symbols are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +10. 8 + + ## Test it end-to-end + + Optional + + Capture a test exception from the same binary the symbols were uploaded for: + + Go + + PostHog AI + + ```go + exception := posthog.NewDefaultException( + time.Now(), + "test_user", + "TestError", + "This is a test exception from Go", + ) + client.Enqueue(exception) + client.Close() + ``` + + Run the binary, then open the [error tracking issues view](https://app.posthog.com/error_tracking). The stack trace should resolve against the uploaded symbols, with source context if you used `--include-source`. Rebuilding changes the binary's identity, so upload again before you test. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-go/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/SKILL.md new file mode 100644 index 00000000..ac221753 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/SKILL.md @@ -0,0 +1,420 @@ +--- +name: error-tracking-upload-source-maps-ios +description: Upload dSYM debug symbols to PostHog Error Tracking for iOS +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for iOS + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/ios.md` - Upload dsyms for ios - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for iOS. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the iOS reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for iOS are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version +- If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/COMMANDMENTS.md new file mode 100644 index 00000000..dd2c05a5 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version +- If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/ios.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/ios.md new file mode 100644 index 00000000..b2575be4 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/ios.md @@ -0,0 +1,286 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload dSYMs for iOS - Docs + +Copy page + +# Upload dSYMs for iOS - Docs + +**CLI version requirement** + +A minimum CLI version of [0.7.7](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.7) is required, but we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md) to ensure you have all of Error Tracking's features. + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Configure build settings + + Required + + In Xcode, configure your build settings to generate dSYMs: + + 1. Open your project in Xcode + 2. Select your target + 3. Go to **Build Settings** + 4. Search for **Debug Information Format** + 5. Make sure Release configurations have **DWARF with dSYM File** + + ![Xcode dSYM setting](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/DWARF_and_d_SYM_xcode_settings_light_f6a8bb74c3.png)![Xcode dSYM setting](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/DWARF_and_d_SYM_xcode_settings_7b2c628b52.png) + + **Disable User Script Sandboxing** + + You must disable User Script Sandboxing for the upload script to work: + + 1. In Build Settings, search for **User Script Sandboxing**(`ENABLE_USER_SCRIPT_SANDBOXING`) + 2. Set `ENABLE_USER_SCRIPT_SANDBOXING` to **No** + + **Why is this required?** + + When User Script Sandboxing is enabled, Xcode only allows run scripts to access files explicitly specified in the build phase's **Input Files**. The dSYM upload script needs to walk directories to locate and read dSYM bundles, and execute `posthog-cli` which are currently not allowed with User Script Sandboxing enabled. + +4. 4 + + ## Add build phase script + + Required + + To symbolicate crash reports, PostHog needs your project's debug symbol (dSYM) files. The following script automatically processes and uploads dSYMs whenever you build your app. + + Add a **Run Script** build phase: + + 1. In Xcode, select your main app target + 2. Go to **Build Phases** tab + 3. Click the **+** button and select **New Run Script Phase** + 4. Make sure it's set to run last (after "Copy Bundle Resources" or similar) + 5. Add the appropriate script for your package manager: + + PostHog AI + + ### Swift + + ```bash + ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + 6. In the **Input Files** section of the Run Script phase, add: + + PostHog AI + + ``` + $(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME) + ``` + + This makes Xcode wait for the app's dSYM contents before running the upload script. + +5. 5 + + ## Optional: Include source code context + + Optional + + By default, only debug symbols are uploaded. To include source code snippets in your stack traces (for better debugging context), set the `POSTHOG_INCLUDE_SOURCE` environment variable: + + 1. In the Run Script build phase, click the chevron to expand + 2. Set the **environment Variable** when calling the upload script: + + PostHog AI + + ### Swift + + ```bash + POSTHOG_INCLUDE_SOURCE=1 ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + POSTHOG_INCLUDE_SOURCE=1 ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + **Note**: This increases upload size and build times. Only enable if you need source code context in PostHog's error tracking UI. + +6. 6 + + ## Optional: Skip conflicting uploads + + Optional + + If PostHog already has a dSYM with the same UUID but different content, the upload fails with `content_hash_mismatch` and stops your build. To skip these conflicting uploads and keep the existing symbols instead, set the `POSTHOG_SKIP_ON_CONFLICT` environment variable when calling the upload script: + + PostHog AI + + ### Swift + + ```bash + POSTHOG_SKIP_ON_CONFLICT=1 ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh + ``` + + ### CocoaPods + + ```bash + POSTHOG_SKIP_ON_CONFLICT=1 ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh + ``` + + Requires posthog-ios 3.64.7 or later and posthog-cli 0.7.12 or later. + +7. 7 + + ## Build and verify + + Required + + Build your app in Xcode. The dSYM upload script will automatically run and upload symbols to PostHog. + + Check the build log output for confirmation. + +8. 8 + + ## Force a test crash + + Optional + + To verify everything is working end-to-end, force a test crash and confirm it appears in PostHog. + + 1. Add a crash trigger button to your app: + + PostHog AI + + ### SwiftUI + + ```swift + Button("Test Crash") { + fatalError("PostHog test crash") + } + ``` + + ### UIKit + + ```swift + let button = UIButton(type: .system) + button.setTitle("Test Crash", for: .normal) + button.addTarget(self, action: #selector(testCrash), for: .touchUpInside) + view.addSubview(button) + @objc func testCrash() { + fatalError("PostHog test crash") + } + ``` + + 2. Build and run your app in Xcode, + + - Make sure the debugger is not attached, since it will intercept the crash and prevent the report from being collected. You can: + - Detach the debugger (click the stop button in Xcode) and run the app directly from the device/simulator home screen + - Or uncheck "Debug executable" in the scheme settings before running + 3. Reopen the app from your device or simulator's home screen (not from Xcode). + + 4. Tap the **Test Crash** button. The app will crash. The exception is collected but **not yet sent** to PostHog. + + 5. Reopen the app once more. This triggers the SDK to send the previously collected crash report to PostHog. + + 6. Check the [error tracking dashboard](https://app.posthog.com/error_tracking) for your test crash. + + Remove the crash trigger button before shipping to production. + +10. ## Verify dSYMs upload + + Checkpoint + + Confirm that dSYMs are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +## Troubleshooting + +### Script fails with "error: posthog-cli not found" + +Install the CLI using one of these methods: + +Terminal + +PostHog AI + +```bash +# NPM global install +npm install -g @posthog/cli +# Or download binary +curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh +``` + +### Script fails with permission errors + +Ensure `ENABLE_USER_SCRIPT_SANDBOXING` is set to `NO` in your build settings. + +### dSYMs not being generated + +Verify that `DEBUG_INFORMATION_FORMAT` is set to **DWARF with dSYM File** for the configuration you're building (Debug or Release). + +### Script fails with "content\_hash\_mismatch" + +This happens when the upload script runs before Xcode finishes generating the dSYM files. Leave **Based on dependency analysis** enabled and add `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` to the **Input Files** of the Run Script build phase. This makes Xcode wait for the app's dSYM contents before running the upload script. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-ios/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/SKILL.md new file mode 100644 index 00000000..0f59dceb --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/SKILL.md @@ -0,0 +1,417 @@ +--- +name: error-tracking-upload-source-maps-nextjs +description: Upload source maps to PostHog Error Tracking for Next.js +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Next.js + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/nextjs.md` - Upload source maps for Next.js - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Next.js. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Next.js reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Next.js are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/COMMANDMENTS.md new file mode 100644 index 00000000..d9b85301 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/COMMANDMENTS.md @@ -0,0 +1,17 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/nextjs.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/nextjs.md new file mode 100644 index 00000000..47912329 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/nextjs.md @@ -0,0 +1,113 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Next.js - Docs + +Copy page + +# Upload source maps for Next.js - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog Next.js config package + + Required + + Terminal + + PostHog AI + + ```shell + npm install @posthog/nextjs-config + ``` + +2. 2 + + ## Add PostHog config to your Next.js app + + Required + + Add the following to your `next.config.js` file: + + next.config.js + + PostHog AI + + ```javascript + import { withPostHogConfig } from "@posthog/nextjs-config"; + const nextConfig = { + //...your nextjs config, + }; + export default withPostHogConfig(nextConfig, { + personalApiKey: process.env.POSTHOG_API_KEY!, // Personal API Key + projectId: process.env.POSTHOG_PROJECT_ID, // Project ID + host: process.env.NEXT_PUBLIC_POSTHOG_HOST, // (optional), defaults to https://us.posthog.com + sourcemaps: { // (optional) + enabled: true, // (optional) Enable sourcemaps generation and upload, default to true on production builds + releaseName: "my-application", // (optional) Release name, defaults to repository name + releaseVersion: "1.0.0", // (optional) Release version, defaults to current git commit + deleteAfterUpload: true, // (optional) Delete sourcemaps after upload, defaults to true + }, + }); + ``` + + Where you should set the following environment variables: + + | Environment Variable | Description | + | --- | --- | + | POSTHOG_API_KEY | [Personal API key](https://app.posthog.com/settings/user-api-keys#variables) with at least write access on error tracking | + | POSTHOG_PROJECT_ID | Project ID you can find in your [project settings](https://app.posthog.com/settings/project#variables) | + | NEXT_PUBLIC_POSTHOG_HOST | Your PostHog instance URL. Defaults to https://us.posthog.com | + + If you are using Vercel (or any other hosting service), make sure these environment variables are added to your project settings, not just your local setup. This enables source maps to be automatically uploaded on your production build. + +3. ## Verify source map generation + + Checkpoint + + Before proceeding, confirm that source maps are being generated by checking for `.js.map` files in your `dist` directory. These are the symbol sets that will be used to unminify stack traces in PostHog. + +4. 3 + + ## Inject and upload source maps + + Required + + The Next.js config package automatically handles source map injection and upload during the build process. No additional configuration is needed. + + Make sure you serve the injected files in production by running the same build and inject process in CI during deployment. PostHog will need access to these injected `chunkId` comments when building the stack traces. + +5. ## Verify source map upload and injection + + Checkpoint + + 1. Confirm that source maps are successfully uploaded to PostHog. + + [Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + + 2. Confirm that the served files are injected with the correct source map comment in production in dev tools: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nextjs/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-node/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/SKILL.md new file mode 100644 index 00000000..7f4d6026 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/SKILL.md @@ -0,0 +1,415 @@ +--- +name: error-tracking-upload-source-maps-node +description: Upload source maps to PostHog Error Tracking for Node.js +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Node.js + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/node.md` - Upload source maps for Node.js - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Node.js. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Node.js reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Node.js are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead +- 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')) +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/COMMANDMENTS.md new file mode 100644 index 00000000..11206d59 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead +- 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')) +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/node.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/node.md new file mode 100644 index 00000000..16626cc6 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/node.md @@ -0,0 +1,166 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Node.js - Docs + +Copy page + +# Upload source maps for Node.js - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Output source maps for Node.js + + Required + + *Your goal in this step: Configure your build to generate source maps.* + + If you serve minified bundles in production, PostHog requires source maps to generate accurate stack traces. Here are instructions to enable source map generation for popular build tools: + + | Build Tool | Documentation | + | --- | --- | + | Vite | [Source Map Configuration](https://v3.vitejs.dev/config/build-options.html#build-sourcemap) | + | webpack | [Source Map Configuration](https://webpack.js.org/configuration/devtool/) | + | Rollup | [Source Map Options](https://rollupjs.org/configuration-options/#output-sourcemap) | + + For other build tools, consult their documentation to enable source maps. + +4. 4 + + ## Inject source map + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + +5. ## Verify source map injection + + Checkpoint + + Confirm that the served files are injected with the correct source map comment in production in dev tools: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +6. 5 + + ## Upload source map + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + + #### Serve injected assets + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. We suggest you upload source maps right after your production build in CI. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +8. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-node/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/SKILL.md new file mode 100644 index 00000000..83f0bb26 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/SKILL.md @@ -0,0 +1,409 @@ +--- +name: error-tracking-upload-source-maps-nuxt +description: Upload source maps to PostHog Error Tracking for Nuxt +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Nuxt + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/nuxt-3-7.md` - Nuxt error tracking installation (v3.7 and above) - docs +- `references/nuxt.md` - Upload source maps for nuxt - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Nuxt. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Nuxt reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Nuxt are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt-3-7.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt-3-7.md new file mode 100644 index 00000000..2a0eb7aa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt-3-7.md @@ -0,0 +1,187 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Nuxt Error Tracking installation (v3.7 and above) - Docs + +Copy page + +# Nuxt Error Tracking installation (v3.7 and above) - Docs + +1. 1 + + ## Install the PostHog Nuxt module + + Required + + Install the PostHog Nuxt module using your package manager: + + PostHog AI + + ### npm + + ```bash + npm install @posthog/nuxt + ``` + + ### yarn + + ```bash + yarn add @posthog/nuxt + ``` + + ### pnpm + + ```bash + pnpm add @posthog/nuxt + ``` + + ### bun + + ```bash + bun add @posthog/nuxt + ``` + + Add the module to your `nuxt.config.ts` file: + + nuxt.config.ts + + PostHog AI + + ```typescript + export default defineNuxtConfig({ + modules: ['@posthog/nuxt'], + // Enable source maps generation in both vue and nitro + sourcemap: { + client: 'hidden' + }, + nitro: { + rollupConfig: { + output: { + sourcemapExcludeSources: false, + }, + }, + }, + posthogConfig: { + publicKey: '', // Find it in project settings https://app.posthog.com/settings/project + host: 'https://us.i.posthog.com', // Optional: defaults to https://us.i.posthog.com. Use https://eu.i.posthog.com for EU region + clientConfig: { + capture_exceptions: true, // Enables automatic exception capture on the client side (Vue) + }, + serverConfig: { + enableExceptionAutocapture: true, // Enables automatic exception capture on the server side (Nitro) + }, + sourcemaps: { + enabled: true, + projectId: '', // Your project ID, found in your environment settings: https://app.posthog.com/settings/environment#variables + personalApiKey: '', // Your personal API key from PostHog settings https://app.posthog.com/settings/user-api-keys (requires organization:read and error_tracking:write scopes) + releaseName: 'my-application', // Optional: defaults to git repository name + releaseVersion: '1.0.0', // Optional: defaults to current git commit + }, + }, + }) + ``` + + **Personal API key** + + Your personal API key will require `organization:read` and `error_tracking:write` scopes. + + The module will automatically: + + - Initialize PostHog on both Vue (client side) and Nitro (server side) + - Capture exceptions on both client and server + - Generate and upload source maps during build + +2. 2 + + ## Manually capturing exceptions + + Optional + + Our module if set up as shown above already captures both client and server side exceptions automatically. + + To send errors manually on the client side, import it and use the `captureException` method like this: + + Vue + + PostHog AI + + ```html + + ``` + + On the server side instantiate PostHog using: + + server/api/example.js + + PostHog AI + + ```javascript + const runtimeConfig = useRuntimeConfig() + const posthog = new PostHog( + runtimeConfig.public.posthogPublicKey, + { + host: runtimeConfig.public.posthogHost, + } + ); + try { + const results = await DB.query.users.findMany() + return results + } catch (error) { + posthog.captureException(error) + } + ``` + +3. 3 + + ## Build your project for production + + Required + + Build your project for production by running the following command: + + Terminal + + PostHog AI + + ```bash + nuxt build + ``` + + The PostHog module will automatically **generate and upload source maps** to PostHog during the build process. + +4. ## Verify error tracking + + Recommended + + *Confirm events are being sent to PostHog* + + Before proceeding, let's make sure exception events are being captured and sent to PostHog. You should see events appear in the activity feed. + + ![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_ouxl_f788dd8cd2.png)![Activity feed with events](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250729_owae_7c3490822c.png) + + [Check for exceptions in PostHog](https://app.posthog.com/activity/explore) + +5. 4 + + ## Upload source maps + + Required + + Great, you're capturing exceptions! If you serve minified bundles, the next step is to upload source maps to generate accurate stack traces. + + Let's continue to the next section. + + [Upload source maps](/docs/error-tracking/upload-source-maps/nuxt.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt.md new file mode 100644 index 00000000..5848c32e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/nuxt.md @@ -0,0 +1,164 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Nuxt - Docs + +Copy page + +# Upload source maps for Nuxt - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Nuxt v3.7 and above + +For Nuxt v3.7 and above, the `@posthog/nuxt` module automatically handles source map generation and upload during the build process. + +No manual configuration is needed – follow the [Nuxt Error Tracking installation guide](/docs/error-tracking/installation/nuxt-3-7.md) to set up the module, and source maps are automatically generated and uploaded when you build your project. + +## Nuxt v3.6 and below + +For older versions of Nuxt, you'll need to manually configure source map generation and upload using the PostHog CLI. + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Generate source maps during build + + Required + + You can hook into the `close` event to generate and upload source maps for your Nuxt application like this: + + nuxt.config.js + + PostHog AI + + ```javascript + import { execSync } from 'child_process' + export default defineNuxtConfig({ + ..., + sourcemap: { + client: true + }, + hooks: { + 'nitro:build:public-assets': async () => { + console.log('Running PostHog sourcemap injection and upload...') + try { + execSync("posthog-cli sourcemap inject --directory '.output'", { + stdio: 'inherit', + }) + execSync("posthog-cli sourcemap upload --directory '.output' --delete-after", { + stdio: 'inherit', + }) + console.log('PostHog sourcemap injection completed successfully') + } catch (error) { + console.error('PostHog sourcemap injection failed:', error) + } + } + } + }) + ``` + +4. 4 + + ## Build your project for production + + Required + + Build your project for production by running the following command: + + Terminal + + PostHog AI + + ```bash + nuxt build + ``` + + Post-build scripts should automatically generate and upload source maps to PostHog. + +5. ## Verify source map upload + + Checkpoint + + *Confirm source maps are being properly uploaded* + + Before proceeding, confirm that source maps are being properly uploaded. + + You can verify the injection is successful by checking your `.mjs` bundle files for `//# chunkId=` comments. The `--delete-after` flag removes the `.map` files after upload so they won't be exposed in your public deployment — PostHog already has them. Make sure to serve the injected JS bundles in production, PostHog will use the `//# chunkId` comments to match them with the uploaded source maps. + + [Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +6. 5 + + ## Next steps + + After configuring PostHog, update your build and deployment process to serve bundles **injected** with the `chunkId` comments. PostHog relies on these comments to display the correct stack traces. Remember to export the [required environment variables](#authenticating-the-posthog-cli) in your deployment process. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-nuxt/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/SKILL.md new file mode 100644 index 00000000..d50f122d --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/SKILL.md @@ -0,0 +1,412 @@ +--- +name: error-tracking-upload-source-maps-react-native +description: Upload source maps to PostHog Error Tracking for React Native +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for React Native + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/react-native.md` - Upload source maps for React native - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for React Native. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the React Native reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for React Native are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-react-native is the React Native SDK package name +- Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) +- react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it +- Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/COMMANDMENTS.md new file mode 100644 index 00000000..a1d09803 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/COMMANDMENTS.md @@ -0,0 +1,12 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-react-native is the React Native SDK package name +- Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) +- react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it +- Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/react-native.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/react-native.md new file mode 100644 index 00000000..93b0740c --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/react-native.md @@ -0,0 +1,452 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for React Native - Docs + +Copy page + +# Upload source maps for React Native - Docs + +**CLI version requirement** + +A minimum CLI version of [0.7.8](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.8) is required, but we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md) to ensure you have all of error tracking's features. + +**React Native Web** + +If you are using React Native Web, ensure source maps are generated + +Terminal + +PostHog AI + +```bash +# example command +# Files are usually under the '$buildFolder/_expo/static/js/web' folder ($name.js and $name.js.map) +npx expo export --source-maps --platform web +``` + +Then use our standard [CLI approach](/docs/error-tracking/upload-source-maps/cli.md) to inject and upload them. + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Inject + + Required + + **Expo 50 or later** + + Automatic source map injection works only in React Native apps using Expo 50 or later, as it relies on Expo’s built-in [debug ID](https://docs.expo.dev/versions/latest/config/metro/#source-map-debug-id) injection. + + Create or update the [metro.config.js](https://docs.expo.dev/versions/latest/config/metro/) file in your app's root directory so that it looks like this: + + diff + + PostHog AI + + ```diff + -const { getDefaultConfig } = require("expo/metro-config"); + +const { getPostHogExpoConfig } = require('posthog-react-native/metro') + -const config = getDefaultConfig(__dirname); + +const config = getPostHogExpoConfig(__dirname) + module.exports = config + ``` + +4. 4 + + ## Upload + + Required + + Automatic source map uploading is handled through the Gradle build process on Android and the Xcode build process on iOS. + + **EAS Updates** + + If you are using [EAS Updates](https://docs.expo.dev/eas-update/introduction/), source maps must be uploaded manually after running the update command. The automatic upload via Gradle/Xcode only runs during native builds, not during OTA updates. + + Terminal + + PostHog AI + + ```bash + # After running `eas update` or `npx expo export --dump-sourcemap`, upload source maps from the output directory + posthog-cli hermes upload --directory dist + ``` + + The `dist` folder is the default output directory for EAS updates. + + 1. Expo: Update the [app.json](https://docs.expo.dev/versions/latest/config/app/#plugins) file in your app's root directory. + + Expo Plugins that add modifications can only be used with [prebuilding](https://docs.expo.dev/workflow/continuous-native-generation/) and managed [EAS Build](https://docs.expo.dev/build/introduction/). + + If you can use the `posthog-react-native/expo` plugin, skip steps 2 and 3. If not, proceed with steps 2 and 3. + + Add the `posthog-react-native/expo` plugin under the `expo.plugins`. + + diff + + PostHog AI + + ```diff + { + "expo": { + ... + "plugins": [ + ... + + "posthog-react-native/expo" + ] + } + } + ``` + + If your project has a checked-in `ios/` directory, run or rerun the iOS prebuild after adding or upgrading the plugin: + + Terminal + + PostHog AI + + ```bash + npx expo prebuild --platform ios + ``` + + This reapplies the config plugin to the existing **Bundle React Native code and images** build phase. When upgrading from `posthog-react-native` 4.61.0 or earlier to a newer version, the plugin automatically migrates the previously generated PostHog command. You do not need to edit `project.pbxproj` manually. + + The plugin accepts the following options: + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | disableSandboxing | boolean | true | iOS only. When true, sets ENABLE_USER_SCRIPT_SANDBOXING=NO on your main app target's Xcode build configurations during expo prebuild. This lets the source map upload script read .git/ (for release metadata) and lets stock Expo projects build without manually declaring input/output files on every script phase. | + | skipOnConflict | boolean | false | Adds --skip-on-conflict to JavaScript source map uploads on iOS and Android. Use this when the same build can upload the same source maps more than once and you want duplicate uploads to be skipped instead of failing. Requires posthog-react-native 4.49.1 or later. When uploadNativeSymbols is enabled, it also applies to native iOS dSYM uploads: a build whose dSYM already exists in PostHog with different content skips the upload and keeps the existing symbols instead of failing. The dSYM behavior requires posthog-react-native 4.56.1 or later, posthog-ios 3.64.7 or later, and posthog-cli 0.7.12 or later. | + | dotenvFile | string | – | Path to a dotenv file containing your POSTHOG_CLI_* credentials (API key, project ID, optional host), relative to the project root or absolute. During expo prebuild the path is wired into every upload hook as POSTHOG_CLI_DOTENV_FILE — an Xcode build setting on iOS and a posthog.dotenvFile entry in android/gradle.properties on Android — so the JavaScript source map, dSYM, and ProGuard/R8 mapping uploads all read credentials from the file regardless of how the native build is started. Real environment variables take precedence and a missing file only logs a warning, so the same config works unchanged in CI. Requires posthog-react-native 4.60.0 or later and posthog-cli 0.8.4 or later (older CLIs ignore the variable). | + + Example opting out of the sandbox change: + + JSON + + PostHog AI + + ```json + { + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "disableSandboxing": false }] + ] + } + } + ``` + + **When to opt out** + + `disableSandboxing: false` is intended for organizations with compliance requirements that forbid disabling Xcode's user script sandbox. With the opt-out: + + - iOS source map uploads continue to work — errors still symbolicate + - Git metadata will not be attached to iOS releases (the UI chip shown for Android rows will be missing on iOS rows) + - Your project must already have every build-phase script (yours, Expo's, CocoaPods-generated) declaring its inputs and outputs, otherwise the build will fail + + This setting only affects build-time scripts on the build machine. Your shipped app's runtime security is unchanged. + + Example skipping duplicate JavaScript source map uploads: + + JSON + + PostHog AI + + ```json + { + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "skipOnConflict": true }] + ] + } + } + ``` + + Example reading CLI credentials from a gitignored `.env` file at build time: + + JSON + + PostHog AI + + ```json + { + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "dotenvFile": ".env" }] + ] + } + } + ``` + + .env + + PostHog AI + + ```bash + POSTHOG_CLI_API_KEY=phx_your_personal_api_key + POSTHOG_CLI_PROJECT_ID=12345 + # EU Cloud only + POSTHOG_CLI_HOST=https://eu.posthog.com + ``` + + 2. Android: Update the `/android/app/build.gradle` file from your app's root directory so that it looks like this: + + diff + + PostHog AI + + ```diff + ... + // This must be placed directly above the following `android` block. + +apply from: new File(["node", "--print", "require('path').join(require('path').dirname(require.resolve('posthog-react-native')), '..', 'tooling', 'posthog.gradle')"].execute().text.trim()) + android { + ... + } + ``` + + 3. iOS: Update the `/ios/$yourProjectName.xcodeproj/project.pbxproj` file (replace `$yourProjectName` with your project's actual name). You need to locate the `Bundle React Native code and images` build step. + + To do this through Xcode, open your project in Xcode, go to the project settings, navigate to Build Phases, and select Bundle React Native code and images. Use the command that matches your installed `posthog-react-native` version. + + #### `posthog-react-native` 4.61.0 and earlier + + diff + + PostHog AI + + ```diff + ... + -`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"` + +/bin/sh `"$NODE_BINARY" --print "require('path').join(require('path').dirname(require.resolve('posthog-react-native')), '..', 'tooling', 'posthog-xcode.sh')"` `"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"` + ``` + + #### `posthog-react-native` newer than 4.61.0 + + diff + + PostHog AI + + ```diff + ... + -`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"` + +`"$NODE_BINARY" --print "require('path').join(require('path').dirname(require.resolve('posthog-react-native')), '..', 'tooling', 'posthog-xcode.sh')"` `"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"` + ``` + + If you are not using the Expo plugin (step 1), you will need to also disable User Script Sandboxing for the upload script to work. In Xcode, go to Build Settings, search for **User Script Sandboxing** (`ENABLE_USER_SCRIPT_SANDBOXING`) and set it to **No**. This is required because the script needs to read `.git/` for release metadata and execute `posthog-cli`, which are not allowed under Xcode's script sandbox. + +5. 5 + + ## Native crash symbolication + + Optional + + The steps above upload **JavaScript** source maps, which symbolicate exceptions thrown in your JS/TS code (including Hermes bytecode). They do **not** cover native iOS and Android crashes. + + If you enable [native crash autocapture](/docs/libraries/react-native.md#native-crash-autocapture) (`errorTracking.autocapture.nativeCrashes`), you also need to upload **native** debug symbols at build time so those crash reports are readable. This reuses the native PostHog SDKs' own tooling: + + - **iOS** – upload dSYMs via posthog-ios's `upload-symbols.sh`, which runs `posthog-cli dsym upload`. You can optionally include native source files for source-code context around crashes. + - **Android** – upload ProGuard/R8 mapping files via the official [`com.posthog.android` Gradle plugin](/docs/error-tracking/upload-mappings/android.md), which also injects a matching map-id into the app. + + **Source-code context is iOS only** + + Including native source files for source-code context (`includeSource` / `POSTHOG_INCLUDE_SOURCE`) is supported on **iOS only**. The Android ProGuard/R8 mapping upload has no source-inclusion equivalent, so the option is ignored on Android. + + **Three pieces are required end-to-end** + + Symbolicated native crashes need all three of these working together: + + 1. **Build-time** – upload native symbols (this section). + 2. **Runtime** – enable [`errorTracking.autocapture.nativeCrashes`](/docs/libraries/react-native.md#native-crash-autocapture) and install `@posthog/react-native-plugin`. + 3. **Server-side** – enable the **Enable exception autocapture** setting in your project's [error tracking settings](https://app.posthog.com/settings/project-error-tracking#exception-autocapture). + + ### Prerequisites + + Both platforms upload through `posthog-cli`, which must be installed and authenticated – the same setup covered in the **Download CLI** and **Authenticate** steps above. In CI/CD builds (such as EAS Build), set the `POSTHOG_CLI_API_KEY` and `POSTHOG_CLI_PROJECT_ID` environment variables (and `POSTHOG_CLI_HOST` for EU). See [authenticating the CLI](/docs/error-tracking/upload-source-maps/cli.md) for the full list. + + For local builds, the easiest way to supply these credentials is the `dotenvFile` plugin option described in the **Upload** step — it covers the native symbol uploads on both platforms too. On bare React Native, you get the same behavior by adding `posthog.dotenvFile=../.env` to `android/gradle.properties` (relative paths resolve against the `android/` directory; the mapping upload needs `posthog-android-gradle-plugin` 1.4.0 or later) and, on iOS, defining a `POSTHOG_CLI_DOTENV_FILE` build setting (for example `$(SRCROOT)/../.env`) on your app target. + + ## Expo + + The `posthog-react-native/expo` config plugin can wire up native symbol upload for you during [prebuild](https://docs.expo.dev/workflow/continuous-native-generation/). It's **opt-in** – set `uploadNativeSymbols` to `true` in your [app.json](https://docs.expo.dev/versions/latest/config/app/#plugins): + + JSON + + PostHog AI + + ```json + { + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "uploadNativeSymbols": true }] + ] + } + } + ``` + + When enabled, `expo prebuild` adds an iOS dSYM-upload build phase and applies the `com.posthog.android` Gradle plugin on Android. + + To additionally upload native source files for source-code context (iOS only), pass an options object instead of `true`: + + JSON + + PostHog AI + + ```json + { + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "uploadNativeSymbols": { "includeSource": true } }] + ] + } + } + ``` + + ## Bare React Native + + If you aren't using the Expo config plugin, wire up the uploads manually. + + #### iOS + + In Xcode, add a **Run Script** build phase to your main app target and place it **last** (so it runs after the dSYM bundle is produced). The `upload-symbols.sh` script ships inside the `PostHog` pod, which is pulled in transitively by `@posthog/react-native-plugin`. + + PostHog AI + + ### CocoaPods + + ```bash + "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh" + ``` + + ### Swift + + ```bash + "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" + ``` + + Then, for your Release configuration, set these build settings: + + - **Debug Information Format** (`DEBUG_INFORMATION_FORMAT`) to **DWARF with dSYM File**, so dSYMs are generated. + - **User Script Sandboxing** (`ENABLE_USER_SCRIPT_SANDBOXING`) to **No**, so the script can locate dSYMs, read `.git/` for release metadata, and run `posthog-cli`. + + To also upload native source files for source-code context (iOS only), prefix the script with `POSTHOG_INCLUDE_SOURCE=1`: + + PostHog AI + + ### CocoaPods + + ```bash + POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh" + ``` + + ### Swift + + ```bash + POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" + ``` + + This is the same flow as the [iOS dSYM upload guide](/docs/error-tracking/upload-source-maps/ios.md), which has a full Xcode walkthrough. + + #### Android + + In `android/build.gradle`, add the [PostHog Android Gradle plugin](/docs/error-tracking/upload-mappings/android.md) to your `buildscript` dependencies (and make sure `mavenCentral()` is in the `buildscript` repositories): + + android/build.gradle + + PostHog AI + + ```gradle + buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("com.posthog:posthog-android-gradle-plugin:1.4.0") + } + } + ``` + + Then apply the plugin in `android/app/build.gradle`: + + android/app/build.gradle + + PostHog AI + + ```gradle + apply plugin: "com.posthog.android" + ``` + + The plugin uploads ProGuard/R8 mapping files and injects a matching map-id during your release build, so native Android crash stack traces are deobfuscated. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +## Troubleshooting + +**Build fails with `Sandbox: bash deny(1) file-read-data`** + +You probably have `{ disableSandboxing: false }` set in your plugin options, or your Xcode project has `ENABLE_USER_SCRIPT_SANDBOXING = YES` set explicitly somewhere that overrides the plugin. Stock Expo projects need the sandbox disabled because several build-phase scripts (Expo's `expo-configure-project.sh`, various CocoaPods-generated scripts) don't declare all their inputs. Either remove the opt-out, or declare inputs on every script phase and keep sandboxing on. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react-native/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/SKILL.md new file mode 100644 index 00000000..774c69e6 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/SKILL.md @@ -0,0 +1,416 @@ +--- +name: error-tracking-upload-source-maps-react +description: Upload source maps to PostHog Error Tracking for React +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for React + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/react.md` - Upload source maps for React - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for React. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the React reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for React are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/COMMANDMENTS.md new file mode 100644 index 00000000..cc803992 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/COMMANDMENTS.md @@ -0,0 +1,16 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/react.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/react.md new file mode 100644 index 00000000..8f08d60a --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/react.md @@ -0,0 +1,175 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for React - Docs + +Copy page + +# Upload source maps for React - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Output source maps for your framework + + Required + + If you're using React with Vite, you can enable source maps in your \`vite.config.js\` file. + + JavaScript + + PostHog AI + + ```javascript + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + export default defineConfig({ + plugins: [react()], + build: { + sourcemap: true, + } + }) + ``` + + If you're using another build tool like [webpack](https://webpack.js.org/configuration/devtool/) or [Rollup](https://rollupjs.org/configuration-options/#output-sourcemap), consult the documentation for your build tool to enable source maps. + +4. 4 + + ## Inject source map + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + +5. ## Verify source map injection + + Checkpoint + + *Confirm source map comments are present* + + Confirm that the served files are injected with the correct source map comment in production in dev tools. + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +6. 5 + + ## Upload source map + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + + #### Serve injected assets + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. We suggest you upload source maps right after your production build in CI. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +8. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-react/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/SKILL.md new file mode 100644 index 00000000..1896a1a6 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/SKILL.md @@ -0,0 +1,408 @@ +--- +name: error-tracking-upload-source-maps-rollup +description: Upload source maps to PostHog Error Tracking for Rollup +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Rollup + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/rollup.md` - Upload source maps for rollup - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Rollup. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Rollup reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Rollup are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/rollup.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/rollup.md new file mode 100644 index 00000000..3db79a98 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/rollup.md @@ -0,0 +1,120 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Rollup - Docs + +Copy page + +# Upload source maps for Rollup - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog Rollup plugin + + Required + + Terminal + + PostHog AI + + ```shell + npm install @posthog/rollup-plugin + ``` + +2. 2 + + ## Add PostHog plugin to your Rollup config + + Required + + Add the following to your `rollup.config.js` file: + + rollup.config.js + + PostHog AI + + ```javascript + import posthog from '@posthog/rollup-plugin' + export default { + input: 'src/index.js', + output: { + dir: 'dist', + format: 'es', + }, + plugins: [ + posthog({ + personalApiKey: process.env.POSTHOG_API_KEY!, // Personal API Key + projectId: process.env.POSTHOG_PROJECT_ID, // Project ID + host: process.env.POSTHOG_HOST, // (optional) defaults to https://us.i.posthog.com + sourcemaps: { // (optional) + enabled: true, // (optional) Enable sourcemaps generation and upload, defaults to true + releaseName: 'my-application', // (optional) Release name + releaseVersion: '1.0.0', // (optional) Release version + deleteAfterUpload: true, // (optional) Delete sourcemaps after upload, defaults to true + }, + }), + ], + } + ``` + + Where you should set the following environment variables: + + | Environment Variable | Description | + | --- | --- | + | POSTHOG_API_KEY | [Personal API key](https://app.posthog.com/settings/user-api-keys#variables) with at least write access on error tracking | + | POSTHOG_PROJECT_ID | Project ID you can find in your [project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_HOST | (optional) Your PostHog instance URL. Defaults to https://us.i.posthog.com | + + If you are using a CI/CD service, make sure these environment variables are added to your project settings, not just your local setup. This enables source maps to be automatically uploaded on your production build. + + **Using @rollup/plugin-typescript?** + + If your build uses [`@rollup/plugin-typescript`](https://www.npmjs.com/package/@rollup/plugin-typescript), set `sourceMap: true` and `inlineSources: true` in your `tsconfig.json` so the PostHog plugin receives the source maps it needs to upload: + + tsconfig.json + + PostHog AI + + ```json + { + "compilerOptions": { + "sourceMap": true, + "inlineSources": true + } + } + ``` + +3. ## Verify source map upload and injection + + Checkpoint + + 1. Confirm that source maps are successfully uploaded to PostHog. + + [Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + + 2. Confirm that the served files are injected with the correct source map comment in production in dev tools: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rollup/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/SKILL.md new file mode 100644 index 00000000..4b0231f1 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/SKILL.md @@ -0,0 +1,414 @@ +--- +name: error-tracking-upload-source-maps-rust +description: Upload native debug symbols to PostHog Error Tracking for Rust +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Rust + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/rust.md` - Upload debug symbols for rust - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Rust. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Rust reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Rust are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/COMMANDMENTS.md new file mode 100644 index 00000000..4eb31257 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/rust.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/rust.md new file mode 100644 index 00000000..9baeaa68 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/rust.md @@ -0,0 +1,223 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload debug symbols for Rust - Docs + +Copy page + +# Upload debug symbols for Rust - Docs + +The [Rust SDK](/docs/error-tracking/installation/rust.md) resolves stack traces in-process from whatever debug info the running binary carries — which works well in development, but release builds omit that debug info by default. Uploading debug symbols gives you fully resolved production stack traces: PostHog symbolicates frames server-side from the exact build that crashed, resolves inlined frames, and can display the source code around each frame. + +**Version requirements** + +- [posthog-rs 0.16.0](https://github.com/PostHog/posthog-rs/releases) or later, which records the instruction addresses server-side symbolication needs. We recommend the latest version. +- [CLI 0.7.32](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.7.32) or later for Linux binaries. Uploading macOS `.dSYM` bundles with the same command needs [CLI 0.8.1](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.8.1) or later, and uploading standalone Mach-O executables (including universal binaries) needs [CLI 0.9.1](https://github.com/PostHog/posthog/releases/tag/posthog-cli%2Fv0.9.1) or later. As always, we recommend [keeping up with the latest CLI version](/docs/error-tracking/upload-source-maps/cli.md). + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Keep debug info in release builds + + Required + + Cargo omits debug info from release builds by default, and there is nothing to upload without it. Enable it in your `Cargo.toml`: + + toml + + PostHog AI + + ```toml + [profile.release] + debug = "line-tables-only" + ``` + + `line-tables-only` is enough to resolve file names, line numbers, and inlined frames while keeping binaries small. Use `debug = "full"` if you want complete debug info. + + On macOS, also set `split-debuginfo = "packed"` in the same profile. Cargo's macOS default (`unpacked`) leaves debug info in intermediate object files instead of producing the `.dSYM` bundle the CLI uploads. + + Watch out for stripping: if your profile sets `strip` explicitly, set it to `"none"` — or strip only after the upload runs. Debug info split into separate files also works: the CLI picks up `objcopy --only-keep-debug` companion files alongside the binaries. + +4. 4 + + ## Check for a build ID + + Required + + PostHog matches stack frames to uploaded symbols by the binary's unique build ID: a GNU build ID on Linux, or the Mach-O UUID on macOS. macOS binaries always carry a UUID, so there is nothing to check there. Most Linux toolchains emit a GNU build ID by default — confirm yours does: + + Terminal + + PostHog AI + + ```bash + readelf -n target/release/my-app | grep "Build ID" + ``` + + Replace `my-app` with your binary's name. + + If nothing shows up, tell the linker to add one in `.cargo/config.toml`, scoped to Linux builds (Apple's linker embeds a UUID on its own and rejects this flag): + + toml + + PostHog AI + + ```toml + [target.'cfg(target_os = "linux")'] + rustflags = ["-C", "link-arg=-Wl,--build-id=sha1"] + ``` + +5. 5 + + ## Build and upload + + Required + + After your release build, point the CLI at the build output directory: + + Terminal + + PostHog AI + + ```bash + cargo build --release + posthog-cli symbol-sets upload --directory target/release + ``` + + The CLI scans the directory and uploads every executable and shared library that carries debug info and a build ID, skipping everything else. On macOS it also uploads `.dSYM` bundles (this needs `dwarfdump`, which ships with Xcode) and standalone Mach-O binaries that embed their own DWARF, splitting universal (fat) binaries into one symbol set per architecture slice. Windows binaries (PDB debug info) are not supported yet. The upload is associated with a [release](/docs/error-tracking/releases.md) automatically when the build directory is inside a git checkout. + + Run this as part of the same pipeline that produces your production binary. Each build has its own build ID, so symbols must be re-uploaded for every build you deploy. + +6. 6 + + ## Optional: Include source code context + + Optional + + By default, only debug symbols are uploaded. To also display the source code around each frame in your stack traces, add `--include-source`: + + Terminal + + PostHog AI + + ```bash + posthog-cli symbol-sets upload --directory target/release --include-source + ``` + + This bundles the project source files referenced by the debug info into the upload. It increases upload size, so only enable it if you want source context in the error tracking UI. + +7. 7 + + ## Optional: Upload from CI + + Optional + + In CI, authenticate with environment variables instead of `posthog-cli login`. For GitHub Actions: + + YAML + + PostHog AI + + ```yaml + - name: Build release binary + run: cargo build --release + - name: Upload debug symbols + run: posthog-cli symbol-sets upload --directory target/release + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + ``` + + Scope the credentials to the upload step only — the build itself doesn't need them. + + The CLI defaults to US Cloud. If you are on EU Cloud or self-hosted, also set `POSTHOG_CLI_HOST` (for example `https://eu.posthog.com`). + + If your binary is built inside a Dockerfile, run the upload in the build stage right after the build, and pass the credentials in as build secrets so they never reach the runtime image. + +9. ## Verify debug symbols upload + + Checkpoint + + Confirm that debug symbols are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +11. 9 + + ## Test it end-to-end + + Optional + + Capture a test exception from the same release binary the symbols were uploaded for: + + Rust + + PostHog AI + + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "This is a test exception from Rust"); + client.capture_exception(&error).await.unwrap(); + ``` + + Run the binary, then check the [error tracking issues view](https://app.posthog.com/error_tracking): the stack trace should resolve to your source files, including source context if you uploaded with `--include-source`. A rebuild changes the build ID, so if you rebuild, upload again before testing. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-rust/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/SKILL.md new file mode 100644 index 00000000..648381bc --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/SKILL.md @@ -0,0 +1,408 @@ +--- +name: error-tracking-upload-source-maps-vite +description: Upload source maps to PostHog Error Tracking for Vite +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Vite + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/vite.md` - Upload source maps for vite - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Vite. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Vite reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Vite are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/vite.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/vite.md new file mode 100644 index 00000000..9465e3fe --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-vite/references/vite.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Vite - Docs + +Copy page + +# Upload source maps for Vite - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog Rollup plugin + + Required + + Vite uses Rollup under the hood, so you can use the PostHog Rollup plugin to upload source maps: + + Terminal + + PostHog AI + + ```shell + npm install @posthog/rollup-plugin + ``` + +2. 2 + + ## Add PostHog plugin to your Vite config + + Required + + Add the PostHog plugin to your `vite.config.js` file: + + vite.config.js + + PostHog AI + + ```javascript + import { defineConfig } from 'vite' + import posthog from '@posthog/rollup-plugin' + export default defineConfig({ + plugins: [ + posthog({ + personalApiKey: process.env.POSTHOG_API_KEY!, // Personal API Key + projectId: process.env.POSTHOG_PROJECT_ID, // Project ID + host: process.env.POSTHOG_HOST, // (optional) defaults to https://us.i.posthog.com + sourcemaps: { // (optional) + enabled: true, // (optional) Enable sourcemaps generation and upload, defaults to true + releaseName: 'my-application', // (optional) Release name + releaseVersion: '1.0.0', // (optional) Release version + deleteAfterUpload: true, // (optional) Delete sourcemaps after upload, defaults to true + }, + }), + ], + }) + ``` + + Set the following environment variables: + + | Environment variable | Description | + | --- | --- | + | POSTHOG_API_KEY | [Personal API key](https://app.posthog.com/settings/user-api-keys#variables) with at least write access on error tracking | + | POSTHOG_PROJECT_ID | Project ID you can find in your [project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_HOST | (optional) Your PostHog instance URL. Defaults to https://us.i.posthog.com | + + **Using CI/CD?** + + Add these environment variables to your CI/CD service's project settings to automatically upload source maps during production builds. + +3. ## Verify source map upload and injection + + Checkpoint + + Confirm source maps were successfully uploaded: + + 1. Go to your [symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) and verify your latest upload appears. + + 2. Check your production JavaScript files in browser dev tools. They should include a source map reference comment: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-web/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/SKILL.md new file mode 100644 index 00000000..328ff830 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/SKILL.md @@ -0,0 +1,419 @@ +--- +name: error-tracking-upload-source-maps-web +description: Upload source maps to PostHog Error Tracking for Web (JavaScript) +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Web (JavaScript) + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/web.md` - Upload source maps for web - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Web (JavaScript). + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Web (JavaScript) reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Web (JavaScript) are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/COMMANDMENTS.md new file mode 100644 index 00000000..8083f7f9 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/COMMANDMENTS.md @@ -0,0 +1,19 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/web.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/web.md new file mode 100644 index 00000000..96b04427 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-web/references/web.md @@ -0,0 +1,170 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for web - Docs + +Copy page + +# Upload source maps for web - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate the PostHog CLI + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + +3. 3 + + ## Output source maps for web + + Required + + If you serve minified bundles in production, PostHog requires source maps to generate accurate stack traces. Here are instructions to enable source map generation for popular build tools: + + | Build Tool | Documentation | + | --- | --- | + | Vite | [Source Map Configuration](https://v3.vitejs.dev/config/build-options.html#build-sourcemap) | + | webpack | [Source Map Configuration](https://webpack.js.org/configuration/devtool/) | + | Rollup | [Source Map Options](https://rollupjs.org/configuration-options/#output-sourcemap) | + + For other build tools, consult their documentation to enable source maps. + +4. 4 + + ## Inject source map + + Required + + *Your goal in this step: Add metadata to associate maps with your code.* + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + +5. ## Verify source map injection + + Checkpoint + + *Confirm source map comments are present* + + Confirm that the served files are injected with the correct source map comment in production in dev tools. + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +6. 5 + + ## Upload source map + + Required + + *Your goal in this step: Send the processed source maps to PostHog.* + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + + #### Serve injected assets + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. We suggest you upload source maps right after your production build in CI. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +8. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/SKILL.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/SKILL.md new file mode 100644 index 00000000..272c21e6 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/SKILL.md @@ -0,0 +1,408 @@ +--- +name: error-tracking-upload-source-maps-webpack +description: Upload source maps to PostHog Error Tracking for Webpack +metadata: + author: PostHog + version: dev +--- + +# Upload source maps to PostHog for Webpack + +This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source. + +## Reference files + +- `references/webpack.md` - Upload source maps for webpack - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/cli.md` - Upload source maps with cli - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +The overview lists every supported framework and build tool. The CLI reference covers `posthog-cli sourcemap process`, which injects chunk IDs and uploads maps in one step. Native binaries (Go, Rust) instead use `posthog-cli symbol-sets upload` — it uploads debug symbols discovered in a build directory, with no inject step; the platform reference covers it. + +## Steps + +The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under **Tips**, and per-technology notes under **Examples**. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Webpack. + +### Get a personal API key + +Source map upload authenticates with a **personal API key**, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page. + +#### Tips +- The public project key (the one in your SDK `init`) will **not** work for uploads — it has no write scope for symbol sets. +- Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file"). +- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. + +### Apply build-config changes + +Wire source map generation, chunk-ID injection, and upload into your **production build** so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a `posthog-cli sourcemap process` step run after the build (it injects chunk IDs and uploads in one pass). Follow the Webpack reference for the exact wiring. + +#### Tips +- If you wire `posthog-cli` directly (no framework or bundler plugin), generating the maps is **your** responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true before `posthog-cli sourcemap process` works: + - Source maps are emitted next to your output bundles (e.g. `.js.map` files). + - The maps include `sourcesContent` (the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved. +- **Inject before deploy**: the *injected* bundles must be the ones shipped to production. Bundles missing the `//# chunkId=…` comment can't be matched to uploaded maps. +- Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code. +- **Don't ship source maps publicly**: omit `.map` files from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. +- **Link each release to its commit.** The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds. + +#### Examples +- **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps. +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload: + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. +- **Android (Gradle)** Android uploads **ProGuard/R8 mapping files**, not source maps. Apply the `com.posthog.android` Gradle plugin on the **app module's** `build.gradle(.kts)` (never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll a `posthog-cli` step. Gotchas: + 1. The plugin only hooks minified variants — if the release build type has `isMinifyEnabled = false`, set it to `true` (keep the existing `proguardFiles` line) or nothing is uploaded. + 2. The upload shells out to `posthog-cli` on the `PATH` (v0.7.4+); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. + 3. The Gradle plugin is versioned separately from the `posthog-android` SDK — never reuse the SDK version in `id("com.posthog.android") version "…"`. +- **Go** Go uploads **native debug symbols**, not source maps, and there is no inject step — the binary's identity (GNU build ID on Linux, Mach-O UUID on macOS) links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory ` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — every build gets its own identity, so re-upload for each deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. On Linux, Go emits no GNU build ID by default — build with `go build -ldflags="-B gobuildid"`. The flag matters at runtime too, not only for upload: without it the SDK can't identify the running binary and falls back to plain runtime-resolved frames. + 2. On macOS, disable DWARF compression instead: `go build -ldflags="-compressdwarf=false"` — symbolication can't read the compressed form (the Mach-O UUID identity is automatic). + 3. Never build with `-ldflags="-s"` or `-ldflags="-w"` (they strip the DWARF, leaving nothing to upload), and avoid `-trimpath` (it rewrites the source paths `--include-source` reads from). + 4. Requires posthog-go 1.22.0+ — older SDKs never emit the instruction addresses and `$debug_images` server-side symbolication needs, so uploaded symbols would sit unused. If go.mod pins an older version, upgrade it as part of this step: `go get github.com/posthog/posthog-go@latest && go mod tidy`. + 5. Windows binaries aren't supported yet — the SDK falls back to plain runtime frames there. +- **Rust (Cargo)** Rust uploads **native debug symbols**, not source maps, and there is no inject step — the build ID baked into the binary links frames to the uploaded symbols. The upload is a standalone CLI step after the build: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` (add `--include-source` so PostHog can show source context around frames). Wire it into the same script/pipeline that produces the production binary — each build has its own build ID, so symbols must be re-uploaded for every deployed build. The wizard pre-installs `posthog-cli` for you, so do not run `npm install -g` yourself. Gotchas: + 1. Release builds omit debug info by default — set `debug = "line-tables-only"` under `[profile.release]` in `Cargo.toml` (enough for file, line, and inline resolution), per the reference. + 2. On macOS also set `split-debuginfo = "packed"` in the same profile — the default leaves debug info in intermediate object files and no `.dSYM` bundle is produced for the CLI to upload. + 3. If the profile sets `strip` explicitly, set it to `"none"` — a stripped binary leaves nothing to upload. + 4. In a Cargo **workspace**, `[profile.*]` settings are only honored in the workspace root `Cargo.toml` — put the debug-info profile there, not in a member crate — and the build output is the workspace-level `target/release`, so point the upload `--directory` at that. Resolve the root with `cargo locate-project --workspace --message-format plain` (prints the root manifest path); the gitignored `.env` belongs next to that root manifest too. +- **Next.js / Nuxt / Angular** Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step. +- **React Native (Expo)** Per the reference: add the `posthog-react-native/expo` plugin entry to `plugins` in `app.json`, and switch `metro.config.js` to `getPostHogExpoConfig` from `posthog-react-native/metro`. The reference badges **native crash symbolication** as *optional* — here it is not: enable `uploadNativeSymbols` with source inclusion on the plugin entry. + Gotchas: + 1. The PostHog wizard installs `posthog-cli` for you — do not run `npm install -g` yourself. + 2. You **must** also enable native crash autocapture (`errorTracking.autocapture.nativeCrashes`) in the SDK setup and install the `@posthog/react-native-plugin` package it depends on — per the reference. +- **Flutter** One upload path per platform directory present (`web/`, `android/`, `ios/`) — wire every one that exists. There is no Dart-level upload. + - **Web** `flutter build web --source-maps`, then `posthog-cli sourcemap process --directory build/web` as a post-build step. + - **Android** Follow the **Android (Gradle)** bullet above, but on `android/app/build.gradle.kts` (never `android/build.gradle.kts`). Flutter's `android/settings.gradle.kts` owns plugin versions: declare `id("com.posthog.android") version "" apply false` there, then apply it versionless in the app module. Skip that bullet's `isMinifyEnabled` step — Flutter always shrinks release builds. + - **iOS** Follow the **iOS (Xcode)** bullet above, on the **Runner** target in `ios/Runner.xcworkspace`. Flutter is always CocoaPods: `${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh`. + + Set `captureNativeExceptions = true` in `PostHogConfig.errorTrackingConfig` — it defaults to `false`, and while it's off the native SDKs capture nothing to symbolicate. + +### Make credentials available at build time + +The upload credentials must be readable **by the build pipeline at build time**, not merely present in a `.env` file. Whether `.env` is auto-loaded depends on the technology. + +#### Tips +- **Auto-loads `.env`**: Next.js, Nuxt and similar frameworks read `.env` into the build for you — nothing extra to do. +- **Vite is a partial exception**: it auto-loads `.env` into `import.meta.env` for client code (only `VITE_`-prefixed vars), but does **not** put vars in `process.env` for your config to read. The upload credentials (`POSTHOG_*`, not `VITE_`-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. +- **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. +- **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (after the bundler), the CLI call is a **separate child process** and will *not* see env vars a loader set inside the bundler config. Point the CLI at the file directly: `posthog-cli --dotenv-file sourcemap process …` (the flag goes before the subcommand). +- **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **Android / Gradle** Gradle does not read `.env` — bridge it in the app module's build script (see the Android example). Unset properties fall back to real `POSTHOG_CLI_*` environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above. +- **React Native (Expo)** Add `"dotenvFile": ".env"` to the `posthog-react-native/expo` plugin entry's options in `app.json` (needs posthog-react-native >= 4.60.0 — bump the package if older). No Xcode or Gradle wiring needed — the plugin handles the native hooks. In CI, set the `POSTHOG_CLI_*` values as job secrets instead. The host var follows the same API-host rule as iOS above. +- **Flutter** One gitignored `.env` at the Flutter project root. Both native sub-projects sit one level down, so they reach *up* for it: + - Web: `posthog-cli --dotenv-file .env sourcemap process --directory build/web` (flag goes **before** the subcommand). + - Android: `rootProject.file("../.env")` — Gradle's root project is `android/`, not the Flutter root. + - iOS: `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/../.env"` — `SRCROOT` is `ios/`. +- **Go / Rust** The upload is always a standalone `posthog-cli` step after the compiler runs, so the separate-process rule applies — pass the dotenv file explicitly (flag before the subcommand): `posthog-cli --dotenv-file .env symbol-sets upload --directory `. The host var follows the same API-host rule as iOS above. + +#### Examples +- **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. +- **Vite** Export `vite.config` as a function and merge `loadEnv` into `process.env` so the config (and the PostHog plugin) can read the upload credentials. Pass `''` as the third arg so non-`VITE_` vars like `POSTHOG_API_KEY` are included — the default `'VITE_'` prefix skips them: + ```ts + import { defineConfig, loadEnv } from 'vite'; + + export default ({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }; + // process.env.POSTHOG_API_KEY is now readable by the plugins below + return defineConfig({ + plugins: [/* … posthog source map plugin … */], + }); + }; + ``` +- **Rollup / webpack / plain Node** Add `import 'dotenv/config'` (or `require('dotenv').config()`) at the top of the config/entry file so the loader runs before the build reads the vars. +- **Standalone posthog-cli step** Pass `--dotenv-file .env` to the `process` invocation so it can authenticate: + ```json + "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" + ``` +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Android (Gradle / posthog-cli)** A gitignored `.env` at the Gradle project root, bridged into the upload tasks in the **app module's** `build.gradle.kts`: + ```kotlin + import com.posthog.android.PostHogCliExecTask + import java.util.Properties + + val postHogEnv = Properties().apply { + val envFile = rootProject.file(".env") + if (envFile.exists()) envFile.inputStream().use { load(it) } + } + + tasks.withType().configureEach { + postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } + postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } + } + ``` + (Groovy `build.gradle`: same shape with `tasks.withType(PostHogCliExecTask).configureEach { … }`.) In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **Go (posthog-cli)** A gitignored `.env` at the module root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory `. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; the build itself does not need the credentials. +- **Rust (Cargo / posthog-cli)** A gitignored `.env` at the crate root, passed straight to the CLI: `posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner — and scope them to the upload step only; `cargo build` runs dependency build scripts and does not need the credentials. + +### Write credentials to the env file + +Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one. + +#### Tips +- Picking the file: if an env file already contains PostHog vars (`POSTHOG_*` / `NEXT_PUBLIC_POSTHOG_*`), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer `.env`. Only create a new file when none exists. +- Variable names depend on which uploader you wired: + - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` + - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` +- Set the `*_HOST` var when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. +- In CI/CD, set the same vars as secrets — never commit the key. + +### Identify the build and run commands + +Resolve two concrete commands for this project: the production **build** command (the one that uploads source maps) and the **run** command that launches the built app (so a test error can be triggered against the real artifact). + +#### Tips +- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app". +- When a build artifact is involved, prefer the command that serves the *production* build over the dev server. + +#### Examples +- **Next.js** Build: `npm run build` (`next build`). Run: `npm run start` (`next start`). +- **Vite** Build: `npm run build`. Run: `npm run preview`. +- **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and the build output dir to name the real file (e.g. `node dist/index.js`). +- **Android** Build: `./gradlew assembleRelease`. Run: launch on a device/emulator (Android Studio, or `./gradlew installRelease`). +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is CI-only. +- **React Native (Expo)** Build + run are one step per platform: `npx expo run:ios --configuration Release` / `npx expo run:android --variant release`. +- **Go** Build: `go build -ldflags="-B gobuildid" -o bin/ . && posthog-cli --dotenv-file .env symbol-sets upload --directory ./bin` (macOS: `-ldflags="-compressdwarf=false"` instead) — the upload is a separate CLI step, so the resolved build command must include it (use the project's Makefile/script target instead when you wired the upload into one). Run: `./bin/`. +- **Rust** Build: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release` — the upload is a separate CLI step, so the resolved build command must include it (use the project's build script/Makefile target instead when you wired the upload into one). Run: `./target/release/` — read the binary name from `Cargo.toml` (the `[package]` name, or a `[[bin]]` entry). +- **Flutter** One pair per platform you wired: + - Web — Build: `flutter build web --source-maps`. Run: `python3 -m http.server 8000 --directory build/web`. Not `flutter run -d chrome` — the dev server skips the upload. + - Android — Build: `flutter build apk --release`. Run: `flutter run --release`. + - iOS — Build: `flutter build ipa`. Run: `flutter run --release`. + +### Set up CI for automatic uploads + +Source maps are only uploaded when the **production build** runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: **find where the production build command actually runs, then make the upload credentials reachable at that exact spot.** **Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file.** Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does **not** automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything: + +1. Is there a `Dockerfile`? If the build command runs inside it (`RUN `), the build happens in that image's **build stage**. +2. Is there a workflow under `.github/workflows/`? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be: + - an inline build step (`run: npm run build`) on the runner, + - a `docker build` / `docker/build-push-action` step (build runs in the image), + - a `uses: ./.github/actions/...` **local composite action** — open that `action.yml`; the real build step is one layer down, + - an `ssh`/deploy step (e.g. `appleboy/ssh-action`) whose `script:` runs the build **on a remote server**. +3. Any other CI config in the repo (`.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. +4. No `Dockerfile`, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples). + +#### Tips +- **A deploy file for another package is not license to author one for this one.** In a monorepo especially, finding a workflow that deploys a *sibling* package (e.g. a `deploy-backend.yml`, or a `Dockerfile`/pipeline for another app) does **not** mean you should create a matching `deploy-frontend.yml` (or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds *this* project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do **not** invent one. +- Reuse the **exact variable names** from "Write credentials to the env file" — the build reads the same names locally and in CI. (`POSTHOG_CLI_*` for direct `posthog-cli`; `POSTHOG_*` for bundler-plugin uploaders.) +- **In CI, credentials travel as environment variables — never as a file.** Do not materialize a `.env` on the runner (e.g. `printf … > .env` before the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes `--dotenv-file .env` to `posthog-cli` works unchanged in CI even though `.env` doesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. +- **Never commit secret values.** Reference credentials by name only: Docker `ARG`/`ENV` or BuildKit secret ids, `${{ secrets.* }}` in GitHub Actions. The personal API key stays out of version control. +- Layers stack — a workflow can call a composite action that runs `docker build` against a Dockerfile. Wire **every** layer the credentials must pass through, from the outer `${{ secrets.* }}` reference down to the `ARG`/`ENV` in the build stage. +- **Multi-stage Dockerfiles:** put the `ARG`/`ENV` in the **build stage** (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). +- **Single-stage Dockerfiles:** with no separate build stage, `ARG`/`ENV` would bake the API key into the shipped image (`docker inspect` reveals `ENV`; `docker history` can reveal build args). Mount the key as a **BuildKit secret** on the build `RUN` instead — it exists for that command only and is never written to a layer (see the single-stage example). Plain `ARG`/`ENV` stays fine for the non-secret project ID and host. +- **Composite / reusable actions can't read `secrets`.** Inside a `.github/actions/*/action.yml` only `${{ inputs.* }}` is available. Add an `inputs:` entry per credential, reference `${{ inputs.* }}` there, and pass `${{ secrets.* }}` from the calling workflow's `with:` block. +- **Build over SSH:** the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the `script:`. `${{ secrets.* }}` is substituted by Actions *before* the script is sent, so the value travels with the script. +- **The worked examples are exemplars, not an allowlist.** For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials + `withCredentials`, …), and cross the same boundaries the same way — Docker builds still need `--build-arg`, SSH sessions still need inline vars. +- **Make only the edits the provider actually needs.** Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need **no functional pipeline change at all**. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where. +- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: **Settings → Secrets and variables → Actions**; GitLab: **Settings → CI/CD → Variables**; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet. + +#### Examples +- **Dockerfile build stage (e.g. `Dockerfile`, no CI)** Declare the credentials as build args and promote them to env vars *before* the build `RUN`, in the build stage: + ```dockerfile + FROM node:22-slim AS build + WORKDIR /app + # ... + ARG POSTHOG_CLI_API_KEY + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ + POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN npm run build # now sees the upload credentials + ``` + With no CI wiring the image, tell the user to pass them when they build: `docker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .` +- **Single-stage Dockerfile (BuildKit secret)** When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep `ARG`/`ENV` for the non-secret project ID and host: + ```dockerfile + # syntax=docker/dockerfile:1 + FROM node:22-slim + WORKDIR /app + # ... + ARG POSTHOG_CLI_PROJECT_ID + ARG POSTHOG_CLI_HOST + ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ + POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST + RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ + npm run build + ``` + Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}`) instead of `build-args:`. The `env=` attribute on `--mount` needs a current BuildKit — keep the `# syntax=docker/dockerfile:1` line; on engines too old for it, read the file form instead: `RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build`. +- **GitHub Actions — inline build step** Build runs on the runner; expose the creds with `env:` on that step: + ```yaml + - name: Build + run: npm run build + env: + POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — `docker build` / `docker/build-push-action`** Add the `ARG`/`ENV` to the Dockerfile build stage (above), then forward the creds as build args. Raw `docker build` takes `--build-arg`; `docker/build-push-action` takes a multi-line `build-args:` input — **merge into the existing `with:` block, don't add a second step**: + ```yaml + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} + POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} + POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — nested/composite action** When the workflow delegates the build with `uses: ./.github/actions/build-and-push`, the `build-push-action` lives in that action's `action.yml`, which can't see `secrets`. Thread them through as inputs. In `.github/actions/build-and-push/action.yml`: + ```yaml + inputs: + posthog-cli-api-key: + required: true + posthog-cli-project-id: + required: true + posthog-cli-host: + required: true + runs: + using: composite + steps: + - uses: docker/build-push-action@v6 + with: + # ...existing context/file/push/tags... + build-args: | + POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} + POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} + POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }} + ``` + Then pass the secrets from the calling workflow's `with:` block: + ```yaml + - uses: ./.github/actions/build-and-push + with: + # ...existing inputs... + posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} + posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} + posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} + ``` +- **GitHub Actions — build over SSH** When a step SSHes into a server and runs the build there (e.g. `appleboy/ssh-action` with `git pull && npm run build`), set the vars inline right before the build command inside the `script:` — mirror however the script already passes runtime vars: + ```yaml + - uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + # ... + script: | + cd /srv/app && git pull --ff-only origin main && npm ci + POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ + POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ + POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ + npm run build + ``` +- **GitLab CI (`.gitlab-ci.yml`)** Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (`script: - npm run build`) needs **no functional YAML change** — no `variables:` block, and do NOT add a script line that writes the variables into a `.env` file (`printf … > .env`, `echo … >> .env`, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off: + ```yaml + build: + stage: build + # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, + # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD + # variables (Settings → CI/CD → Variables); GitLab injects them into + # the job automatically. Mark them Masked — but Protected only if this + # job runs exclusively on protected branches, otherwise feature-branch + # builds fail with missing credentials. + script: + - npm ci + - npm run build + ``` + Then tell the user to add those variables in **Settings → CI/CD → Variables** and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runs `docker build` must forward them (`--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …`) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. +- **Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …)** Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes. +- **Untraceable setup** No `Dockerfile`, no CI config, and no build step you can trace: make no CI changes — do **not** author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (`POSTHOG_CLI_*` / `POSTHOG_*`) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. a `Dockerfile` built by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the `--build-arg` flags). + +### Associate the release with a git commit + +`posthog-cli` links the release to a **git commit, branch and repo** so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local `.git` directory — you never touch the CLI invocation itself (it's usually baked into `npm run build` or a bundler plugin), you just make the git context available in the build environment. A `docker build` is where this breaks: it sees **neither** the env vars nor `.git` (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in. + +#### Tips +- **Forward GitHub's git env vars into the Docker build** the same way you forwarded credentials. Declare each as an `ARG` **and** promote it to `ENV` — `ARG` alone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no `.git`. + +#### Examples +- **GitHub Actions → docker build** Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit: + ```yaml + build-args: | + GITHUB_ACTIONS=true + GITHUB_SHA=${{ github.sha }} + GITHUB_REF_NAME=${{ github.ref_name }} + GITHUB_REPOSITORY=${{ github.repository }} + GITHUB_SERVER_URL=${{ github.server_url }} + ``` + Then in the build stage, declare each as `ARG` and re-export it as `ENV` before the build runs. +- **Inline CI build (no Docker)** GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass. + +### Test the local setup + +Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards. + +#### Tips +- The handler must call the SDK's exception-capture method **directly** — do **not** `throw`. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. +- Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved. +- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. +- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- The upload only happens on the *production build*: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths. + +#### Examples +- **Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup)** Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls `posthog.captureException(new Error("PostHog source maps test"))`. +- **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.captureException(new Error("PostHog source maps test"))` and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. +- **React Native** Add a visible `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. Test flow — the upload only runs on the **Release** build: use the Release run command from "Identify the build and run commands", launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick handler is exactly: + ```kotlin + import com.posthog.PostHog + + PostHog.captureException(Throwable("PostHog source maps test")) + ``` + Test flow — the upload only runs on the **minified release variant**: `./gradlew installRelease` (or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + (`capture()` takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (no `xcodebuild`): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. +- **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(error: Exception("PostHog source maps test"), stackTrace: StackTrace.current)` — arguments are **named**, and `stackTrace` is what the trace resolves against. Give the user a test flow for **every** platform wired, using that platform's build/run pair. +- **Go** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```go + client.Enqueue(posthog.NewDefaultException( + time.Now(), "test_user", "TestError", "PostHog source maps test", + )) + ``` + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly, then run the binary and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the binary's identity, so after any rebuild, re-upload before testing. +- **Rust** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that captures one error and returns 200; with no HTTP layer, add the capture where the client is initialised. The capture is: + ```rust + let error = std::io::Error::new(std::io::ErrorKind::Other, "PostHog source maps test"); + client.capture_exception(&error).await.unwrap(); + ``` + Mirror how the project already calls the client: with the blocking client (`default-features = false` with `features = ["error-tracking"]` added back), drop the `.await`. + Test flow — the binary you run must be the one whose symbols were uploaded. Use the wired build-and-upload script if one exists; otherwise run both steps explicitly: `cargo build --release && posthog-cli --dotenv-file .env symbol-sets upload --directory target/release`, then run `./target/release/` and trigger the capture. It's an event, not a crash — the process keeps running. A rebuild changes the build ID, so after any rebuild, re-upload before testing. + +### Verify and hand off + +Confirm the upload landed and report what changed. + +#### Tips +- Source maps upload during the **production build** — the build must actually run for a symbol set to appear. +- Verify in PostHog Error Tracking settings on the **Symbol sets** page: a new symbol set should appear after the build completes. +- When handing off, list the files you edited (paths only), the env-var **key** names you set (never values), whether a test affordance was added and reverted, and the exact build command to run. +- If you wired CI, list the pipeline files you changed (`Dockerfile`, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced. + +## General tips +- The reference files for Webpack are authoritative — if this page and a reference disagree on an API, follow the reference. +- Two different keys, two different jobs: a **personal API key** uploads maps at build time; the **public project key** powers the SDK at runtime. Don't swap them. +- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve. +- Uploaded maps live in PostHog and never need to be served publicly. +- Detect the project's package manager before installing any dependency. +- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/cli.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/cli.md new file mode 100644 index 00000000..51bfb013 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/cli.md @@ -0,0 +1,150 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps with CLI - Docs + +Copy page + +# Upload source maps with CLI - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Download CLI + + Required + + Install `posthog-cli`: + + PostHog AI + + ### Npm + + ```bash + npm install -g @posthog/cli + ``` + + ### Curl + + ```bash + curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh + posthog-cli-update + ``` + +2. 2 + + ## Authenticate + + Required + + To authenticate the CLI, call the `login` command. This opens your browser where you select your organization, project, and API scopes to grant: + + Terminal + + PostHog AI + + ```bash + posthog-cli login + ``` + + If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate: + + | Environment Variable | Description | Source | + | --- | --- | --- | + | POSTHOG_CLI_HOST | The PostHog host to connect to [default: https://us.posthog.com] | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_PROJECT_ID | PostHog project ID | [Project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_CLI_API_KEY | Personal API key with error tracking write and organization read scopes | [API key settings](https://app.posthog.com/settings/user-api-keys#variables) | + + You can also use the `--host` option instead of the `POSTHOG_CLI_HOST` environment variable to target a different PostHog instance or region. For EU users: + + Terminal + + PostHog AI + + ```bash + posthog-cli --host https://eu.posthog.com [CMD] + ``` + + If you already keep your project's configuration in a dotenv-style file, you can load these variables from it with the `--dotenv-file` option instead of exporting them: + + Terminal + + PostHog AI + + ```bash + posthog-cli --dotenv-file .env sourcemap upload --directory ./path/to/assets + ``` + +3. 3 + + ## Inject + + Required + + Once you've built your application and have bundled assets, inject the context required by PostHog to associate the maps with the served code. + + Terminal + + PostHog AI + + ```bash + # Inject release and chunk metadata into sourcemaps + posthog-cli sourcemap inject --directory ./path/to/assets + ``` + + You can verify that the metadata has been injected by checking for the `//# chunkId=...` comment in the minified code. + +4. 4 + + ## Upload + + Required + + You will then need to upload the modified assets to PostHog. + + Terminal + + PostHog AI + + ```bash + # Upload injected sourcemaps to their release + posthog-cli sourcemap upload --directory ./path/to/assets --release-name my-app --release-version 1.2.3 --build 42 + ``` + + The CLI will create or reuse the [release](/docs/error-tracking/releases.md) for the detected or supplied release name and version. The CLI will try to detect release name and version information, but you can set them explicitly with `--release-name` and `--release-version`. We recommend setting the release name, and letting the CLI detect the version, if your project is continuously deployed (the version will be the git commit hash at build time). + + You can also pass `--build` to record a build number (e.g. `CFBundleVersion` on iOS, `versionCode` on Android) as release metadata. This is optional — when omitted, no build info is recorded. + + > **💡 Tip:** You can use `--delete-after` option to clean up sourcemaps after uploading them. + +5. 5 + + ## Serve injected assets + + Required + + You *must* serve the injected assets in deployed production app. The injected metadata is used during error capture to identify the correct source map to use. + + If you serve a copy of the bundled assets as they were prior to running `posthog-cli sourcemap inject`, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces. + +7. ## Verify source maps upload + + Checkpoint + + Confirm that source maps are successfully uploaded to PostHog.[Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/webpack.md b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/webpack.md new file mode 100644 index 00000000..10b66110 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-upload-source-maps-webpack/references/webpack.md @@ -0,0 +1,99 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps for Webpack - Docs + +Copy page + +# Upload source maps for Webpack - Docs + +## AI wizard + +Set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +## Manual setup + +1. 1 + + ## Install the PostHog Webpack plugin + + Required + + Terminal + + PostHog AI + + ```shell + npm install @posthog/webpack-plugin + ``` + +2. 2 + + ## Add PostHog plugin to your Webpack config + + Required + + Add the following to your `webpack.config.js` file: + + webpack.config.js + + PostHog AI + + ```javascript + const { PosthogWebpackPlugin } = require('@posthog/webpack-plugin') + module.exports = { + // ... your existing config + plugins: [ + new PosthogWebpackPlugin({ + personalApiKey: process.env.POSTHOG_API_KEY, // Personal API Key + projectId: process.env.POSTHOG_PROJECT_ID, // Project ID + host: process.env.POSTHOG_HOST, // (optional) defaults to https://us.i.posthog.com + sourcemaps: { // (optional) + enabled: true, // (optional) Enable sourcemaps generation and upload, defaults to true + releaseName: 'my-application', // (optional) Release name + releaseVersion: '1.0.0', // (optional) Release version + deleteAfterUpload: true, // (optional) Delete sourcemaps after upload, defaults to true + }, + }), + ], + } + ``` + + Where you should set the following environment variables: + + | Environment Variable | Description | + | --- | --- | + | POSTHOG_API_KEY | [Personal API key](https://app.posthog.com/settings/user-api-keys#variables) with at least write access on error tracking | + | POSTHOG_PROJECT_ID | Project ID you can find in your [project settings](https://app.posthog.com/settings/project#variables) | + | POSTHOG_HOST | (optional) Your PostHog instance URL. Defaults to https://us.i.posthog.com | + + If you are using a CI/CD service, make sure these environment variables are added to your project settings, not just your local setup. This enables source maps to be automatically uploaded on your production build. + +3. ## Verify source map upload and injection + + Checkpoint + + 1. Confirm that source maps are successfully uploaded to PostHog. + + [Check symbol sets in PostHog](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-symbol-sets) + + 2. Confirm that the served files are injected with the correct source map comment in production in dev tools: + + JavaScript + + PostHog AI + + ```javascript + //# chunkId=0197e6db-9a73-7b91-9e80-4e1b7158db5c + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-web/SKILL.md b/skills/posthog/all/skills/error-tracking-web/SKILL.md index c3830cae..7beff826 100644 --- a/skills/posthog/all/skills/error-tracking-web/SKILL.md +++ b/skills/posthog/all/skills/error-tracking-web/SKILL.md @@ -3,7 +3,7 @@ name: error-tracking-web description: PostHog error tracking for Web (JavaScript) metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog error tracking for Web (JavaScript) @@ -18,6 +18,7 @@ This skill helps you add PostHog error tracking to Web (JavaScript) applications - `references/monitoring.md` - Monitor and search issues - docs - `references/assigning-issues.md` - Assign issues to teammates - docs - `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,14 +32,18 @@ Consult the documentation for API details and framework-specific patterns. ## Framework guidelines -- Remember that source code is available in the node_modules directory -- Check package.json for type checking or build scripts to validate changes +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). - posthog-js is the JavaScript SDK package name - posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) - posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) -- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Do NOT disable autocapture unless the user explicitly requests it. +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. - NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content - PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties - Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in -- Call posthog.reset() on logout to unlink future events from the current user +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts - For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-web/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-web/references/COMMANDMENTS.md new file mode 100644 index 00000000..8083f7f9 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-web/references/COMMANDMENTS.md @@ -0,0 +1,19 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/error-tracking-web/references/alerts.md b/skills/posthog/all/skills/error-tracking-web/references/alerts.md index 94653a53..a760ac4e 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/alerts.md +++ b/skills/posthog/all/skills/error-tracking-web/references/alerts.md @@ -1,12 +1,18 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + # Send error tracking alerts - Docs To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. ## Issue created or reopened -To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. -![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_light_1a05deef21.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/error_alerts_create_dark_7585087b18.png) +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. @@ -52,11 +58,11 @@ This sends an email notification to the user you choose. Check out our [alerts d **Can't find your alert?** -If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/project/2#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-web/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-web/references/assigning-issues.md index 77bb0f72..fe4ddaaa 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/assigning-issues.md +++ b/skills/posthog/all/skills/error-tracking-web/references/assigning-issues.md @@ -1,26 +1,40 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + # Assign issues to teammates - Docs Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. ## Assign issues -You can manually assign issues as you triage them in the UI. This can be done both in the issue list and issue detail pages. +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. -![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_light_109b2bf454.png)![Error tracking assignment UI](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_ui_dark_4682b2ac80.png) +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. -1. In your error tracking [issue list](https://app.posthog.com/error_tracking), click the **unassigned** selector under each issue to assign it to a role or user. +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) -2. On the detail page of each issue, click the **Assignee** selector to assign it to a role or user. +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). -![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_light_6c7ea17be9.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/roles_dark_f721b94577.png) +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) ## Automatic issue assignment -You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking?activeTab=configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) -![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_light_1cf9a2437a.png)![Error tracking auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/assignment_rules_dark_11e0830b0c.png) +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. @@ -58,19 +72,31 @@ A common use case for automatic issue assignment is to alert assignees of new is ## Create external issues -You can also create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. -First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. Then, from an issue's details page, under **External references**, click **Create issue**. +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. ![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) -The new issue will have a partial stack trace and a link to the issue in PostHog. +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. > If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-web/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-web/references/fingerprints.md index 324758ce..d58a6781 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/fingerprints.md +++ b/skills/posthog/all/skills/error-tracking-web/references/fingerprints.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + # Fingerprints - Docs Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. @@ -48,9 +54,9 @@ Fingerprints can be manually set during exception capture. This is a very useful You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-web/references/monitoring.md b/skills/posthog/all/skills/error-tracking-web/references/monitoring.md index d7383fdd..6590cab2 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/monitoring.md +++ b/skills/posthog/all/skills/error-tracking-web/references/monitoring.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + # Monitor and search issues - Docs This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). @@ -45,11 +51,11 @@ The search bar provides two modes of filtering: This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: -![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_light_2b2dd25208.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/filtering_issues_dark_17d1e67da6.png) +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) Added property filters look like this: -![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/added_property_filter_1e823a16e9.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/property_filter_added_dark_2d4c065baa.png) +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. @@ -103,7 +109,7 @@ This page shows you the following: - Name, description, status, assignee, and external tracking links for the issue. - A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. -![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_light_405a3332d7.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_no_filter_dark_dfce08c4b0.png) +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) ### Filtering exception occurrences within an issue @@ -111,7 +117,7 @@ Once you've found and opened the issue you want to investigate, you can use the For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: -![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_light_0fc6e1cb2d.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/issue_exception_with_filter_dark_bf74dca1d9.png) +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) **Alerts** @@ -131,9 +137,9 @@ If you find your queries timing out or taking more than 30 seconds, please [let If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-web/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-web/references/upload-source-maps.md index 178a4ab8..b6ae318f 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/upload-source-maps.md +++ b/skills/posthog/all/skills/error-tracking-web/references/upload-source-maps.md @@ -1,10 +1,26 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + # Upload source maps - Docs If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. -Choose your platform to view specific instructions. +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) @@ -24,8 +40,14 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + - [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) - [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) @@ -36,9 +58,9 @@ Choose your platform to view specific instructions. - [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-web/references/web.md b/skills/posthog/all/skills/error-tracking-web/references/web.md index b2e9437e..b54aaa8d 100644 --- a/skills/posthog/all/skills/error-tracking-web/references/web.md +++ b/skills/posthog/all/skills/error-tracking-web/references/web.md @@ -1,4 +1,10 @@ -# Web error tracking installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Web Error Tracking installation - Docs + +Copy page + +# Web Error Tracking installation - Docs 1. 1 @@ -18,10 +24,10 @@ ```html ``` @@ -58,7 +64,7 @@ import posthog from 'posthog-js' posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30' }) ``` @@ -134,9 +140,9 @@ [Upload source maps](/docs/error-tracking/upload-source-maps/web.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/error-tracking-wordpress/SKILL.md b/skills/posthog/all/skills/error-tracking-wordpress/SKILL.md new file mode 100644 index 00000000..c54c7f28 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/SKILL.md @@ -0,0 +1,50 @@ +--- +name: error-tracking-wordpress +description: PostHog error tracking for WordPress +metadata: + author: PostHog + version: dev +--- + +# PostHog error tracking for WordPress + +This skill helps you add PostHog error tracking to WordPress applications. + +## Reference files + +- `references/php.md` - Php error tracking installation - docs +- `references/wordpress.md` - How to set up wordpress analytics with PostHog - docs +- `references/fingerprints.md` - Fingerprints - docs +- `references/alerts.md` - Send error tracking alerts - docs +- `references/monitoring.md` - Monitor and search issues - docs +- `references/assigning-issues.md` - Assign issues to teammates - docs +- `references/upload-source-maps.md` - Upload source maps - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys and host URLs. Never hardcode them. +- **Minimal changes**: Add error tracking alongside existing error handling. Don't replace or restructure existing error handling code. +- **Autocapture first**: Enable exception autocapture in the SDK initialization before adding manual captures. +- **Source maps**: Upload source maps so stack traces resolve to original source code, not minified bundles. +- **Manual capture for boundaries**: Use `captureException()` at error boundaries and catch blocks for errors that don't propagate to the global handler. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Ship the integration as a standalone plugin in wp-content/plugins - do NOT edit a theme's functions.php, which is lost on theme switch or theme update +- Guard every plugin entry file with `if (!defined('ABSPATH')) { exit; }` before any other code +- Print the client snippet from a wp_head hook and escape the interpolated token with esc_js() - never echo a raw token into markup +- Read the project token from a wp-config.php constant or a WordPress option - do NOT hardcode it in plugin source +- Capture pageviews with the client SDK and reserve PostHog::capture for real server-side WordPress actions (comment_post, woocommerce_thankyou, user_register) +- Call PostHog::flush() at the end of a capture - a web request has no single exit point like a CLI script does +- Evaluate feature flags client-side on pages served by full-page caching (Varnish, batcache, WP Super Cache) - server-side flag checks are baked into the cached HTML for every visitor +- WordPress catches fatals with its own WP_Fatal_Error_Handler and renders the recovery-mode page, so a fatal can be swallowed before a PHP handler reports it - capture exceptions explicitly with PostHog::captureException in a try/catch, and do not rely on WP_DEBUG being on in production to surface them +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/COMMANDMENTS.md b/skills/posthog/all/skills/error-tracking-wordpress/references/COMMANDMENTS.md new file mode 100644 index 00000000..9eb72479 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/COMMANDMENTS.md @@ -0,0 +1,19 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Ship the integration as a standalone plugin in wp-content/plugins - do NOT edit a theme's functions.php, which is lost on theme switch or theme update +- Guard every plugin entry file with `if (!defined('ABSPATH')) { exit; }` before any other code +- Print the client snippet from a wp_head hook and escape the interpolated token with esc_js() - never echo a raw token into markup +- Read the project token from a wp-config.php constant or a WordPress option - do NOT hardcode it in plugin source +- Capture pageviews with the client SDK and reserve PostHog::capture for real server-side WordPress actions (comment_post, woocommerce_thankyou, user_register) +- Call PostHog::flush() at the end of a capture - a web request has no single exit point like a CLI script does +- Evaluate feature flags client-side on pages served by full-page caching (Varnish, batcache, WP Super Cache) - server-side flag checks are baked into the cached HTML for every visitor +- WordPress catches fatals with its own WP_Fatal_Error_Handler and renders the recovery-mode page, so a fatal can be swallowed before a PHP handler reports it - capture exceptions explicitly with PostHog::captureException in a try/catch, and do not rely on WP_DEBUG being on in production to surface them +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/alerts.md b/skills/posthog/all/skills/error-tracking-wordpress/references/alerts.md new file mode 100644 index 00000000..a760ac4e --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/alerts.md @@ -0,0 +1,69 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Send error tracking alerts - Docs + +Copy page + +# Send error tracking alerts - Docs + +To stay on top of issues, you can set up alerts. These enable you to post to Slack, Discord, Teams, or an HTTP Webhook when an issue is created or reopened. + +## Issue created or reopened + +To alert when an issue is created or reopened, go to [error tracking's configuration page](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-alerting) and click **Alerting**. This shows you a list of existing alerts. Clicking **New notification** brings you to a page to create a new one. + +![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_03_05_339_Z_fce7707d31.png)![Error tracking alerting](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T14_02_44_265_Z_400e53c07a.png) + +Choosing an option brings you to a page to configure the alert. This may require setting up the Slack integration or pasting in a webhook URL. Once done, you can test the alert by clicking **Test function** and then finalize by clicking **Create & enable**. + +This will then send alerts to your chosen destination when an issue is created or reopened like this: + +## Issue properties and assignments + +You can filter an alert based on the properties of an issue. This is useful for notifying a specific team when they have been auto assigned an issue using [auto assignment rules](/docs/error-tracking/managing-issues.md#auto-assignment-rules). + +![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_light_e575af6512.png)![Error tracking alert assignee filtering](https://res.cloudinary.com/dmukukwp6/image/upload/assignee_filter_dark_9a8907af03.png) + +## Spike alerts + +PostHog can also alert you when an existing issue suddenly spikes in volume - for example, after a bad deploy. This works differently from issue-created alerts. Instead of triggering when a new issue is first seen, spike alerts fire when an issue's error rate significantly exceeds its historical baseline. + +See the [spike detection guide](/docs/error-tracking/spikes.md) to learn how it works and how to configure it. + +## Other alerting options + +Since error tracking works by capturing `$exception` events, PostHog features that trigger by events can play a role in alerts too. + +### Real time destinations + +The first way is using [real time destinations](/docs/cdp/destinations.md). This enables you to send events (like `$exception`) to other tools as soon as they are ingested. + +To create a real time destination, go to the [data pipelines tab](https://app.posthog.com/data-management/destinations) in PostHog, click **\+ New**, and then select **Destination**. Choose your destination and press **\+ Create**. + +On the destination creation screen, make sure to add an event matcher for the `$exception` event, filter for the properties you want, and set the trigger options. + +![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_light_9898376c56.png)![Real time destination](https://res.cloudinary.com/dmukukwp6/image/upload/http_error_alert_dark_7705f0e575.png) + +Check out our [real time destinations docs](/docs/cdp/destinations.md) for more information. + +### Trend alerts + +You can also visualize your `$exception` events using [trends](/docs/product-analytics/trends/overview.md). Once you create a trend insight, click the **Alerts** button at the top of the insight and then **New alert**. + +Here you can set alerts for event volume value, increase, or decrease. + +![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_26_43_2x_4ef6402556.png)![Insight alert](https://res.cloudinary.com/dmukukwp6/image/upload/Clean_Shot_2025_04_08_at_14_25_35_2x_f36353143b.png) + +This sends an email notification to the user you choose. Check out our [alerts docs](/docs/alerts.md) for more information. + +**Can't find your alert?** + +If you'd like a destination to be added that we don't yet support, [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3A%3Afalse). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/assigning-issues.md b/skills/posthog/all/skills/error-tracking-wordpress/references/assigning-issues.md new file mode 100644 index 00000000..fe4ddaaa --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/assigning-issues.md @@ -0,0 +1,103 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Assign issues to teammates - Docs + +Copy page + +# Assign issues to teammates - Docs + +Error tracking enables you to assign issues to specific PostHog [roles](https://app.posthog.com/settings/organization-roles) or teammates. This helps your team find relevant issues through **filtering**. You can also set up team-specific **alerting** to notify them when assigned issues are created or reopened. + +## Assign issues + +You can manually assign issues as you triage them in the UI, either from the issue list or an issue's details page. + +From your error tracking [issue list](https://app.posthog.com/error_tracking), click the **Unassigned** selector under any issue to assign it to a role or user. + +![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_16_655_Z_b73751c99d.png)![Assigning an issue from the issue list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_52_59_843_Z_d3d394bf7e.png) + +Alternatively, open an issue and click the **Assignee** selector on its details page. + +![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_53_43_196_Z_4fe353e323.png)![Assigning an issue from its details page](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_54_12_467_Z_35902a2f98.png) + +Want to assign issues to a **team** rather than an individual teammate? You can create a role in [your project settings](https://app.posthog.com/settings/organization-roles). + +![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_26_069_Z_ecff46f618.png)![Error tracking role assignees](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_55_55_647_Z_085f6efe19.png) + +## Automatic issue assignment + +You can set up automatic issue assignment through a set of rules. This can be configured in the [error tracking settings](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-auto-assignment) using **auto assignment rules**. You can also create assignment rules programmatically using the [PostHog MCP server](/docs/error-tracking/surfaces/mcp.md). + +The settings show a list of your existing assignment rules: + +![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_57_06_125_Z_a7f920a3dc.png)![List of auto assignment rules](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_56_47_967_Z_3ddddd1841.png) + +When adding or editing a rule, you can test it before saving. Click **Test** to see how many exceptions matched the rule's conditions over the last 7 days, so you can confirm it behaves as expected. + +![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_30_887_Z_7a01202bc4.png)![Adding an auto assignment rule](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_30_54_381_Z_d468514190.png) + +Assignment conditions are evaluated against the properties of the exception event that created the issue. Because assignment rules are evaluated during ingestion, the stack trace (if present) will be unminified, which enables filtering on exception properties such as function name and source file. + +Issues can be automatically assigned to a **role** or **user** by configuring a set of filters. These filters can be configured to match **any** or **all** of the criteria. + +You can configure automatic assignment to filter on any [event property](/docs/data/events.md) in PostHog. When there are multiple values for a property, the filters return true if it matches **any** of the values. For example, if you have multiple `exception_functions` values, the filters returns true if it matches **any** of the functions. + +Here are some common properties you can filter on: + +| Property | Event property | Description | +| --- | --- | --- | +| Exception type | $exception_types | The type of exception(s) that occurred | +| Exception message | $exception_values | The message(s) detected on the error | +| Exception function | $exception_functions | The function(s) where the exception occurred | +| Exception source | $exception_sources | The source file(s) where the exception occurred | +| Exception was handled | $exception_handled | Whether the exception was handled by the application | +| Device type | $device_type | The type of device that the error occurred on | +| Browser | $browser | The browser that the error occurred in | +| Current URL | $current_url | The URL that the error occurred on | +| Feature flag | $feature_flag | The feature flag that the error occurred on | + +You can also set custom properties on the error tracking event to filter on. For example, setting a custom `params_received` property to provide more context or debug information. + +### Order of issue assignment rules + +Issue assignment filters are evaluated in the order they are configured. They can also be reordered once created. The first filter that matches is used to assign the issue. This means you should configure the most specific filters first, and then the more general filters later. + +### Disabled assignment rules + +Assignment rules can become disabled if an error occurs during ingestion. When a rule is disabled, a banner displays the original error message. To re-enable the rule, edit it to fix the problem and save your changes. If the issue persists, reach out to support. + +### Alerting based on assignment + +A common use case for automatic issue assignment is to alert assignees of new issues. Once the issues are automatically assigned, you can set up alerts to notify the assignee. See the [alerts](/docs/error-tracking/alerts.md) guide for more information. + +## Create external issues + +You can create issues in external tracking systems like GitHub Issues, Linear, GitLab, or Jira. This links PostHog error tracking issues to your existing issue tracking workflows. + +First, set up an [integration](/docs/error-tracking/integrations.md) with your tracking system. + +### From the UI + +From an issue's details page, under **External references**, click **Create issue**. + +![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_light_b89cd91da1.png)![Error tracking create issue in external tracking system](https://res.cloudinary.com/dmukukwp6/image/upload/create_issue_error_dark_7d158087f8.png) + +The new issue has a partial stack trace and a link to the issue in PostHog. + +### Via the API + +You can also create external references programmatically using the [PostHog API](/docs/api.md) with a [personal API key](/docs/api.md#personal-api-keys) that has the `error_tracking:write` scope. + +### Via MCP + +AI agents using the [PostHog MCP server](/docs/model-context-protocol.md) can create external references with the `error-tracking-external-references-create` tool. See the [MCP debugging guide](/docs/error-tracking/surfaces/mcp.md) for more. + +> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/fingerprints.md b/skills/posthog/all/skills/error-tracking-wordpress/references/fingerprints.md new file mode 100644 index 00000000..d58a6781 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/fingerprints.md @@ -0,0 +1,63 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Fingerprints - Docs + +Copy page + +# Fingerprints - Docs + +Every captured exception is assigned a fingerprint. This fingerprint is used to group similar exceptions into issues. This page covers how fingerprints are generated, how they're used, and how you can override them when capturing exceptions. + +## Fingerprint and issue grouping + +Every exception has a fingerprint, whether generated or defined by the user. Each fingerprint links to exactly one issue. Exceptions that share the same fingerprint define an issue. + +Multiple different fingerprints can point to the same issue (a many-to-one relationship) if you [merge issues](/docs/error-tracking/managing-issues.md#merging-issues). + +## How are fingerprints generated? + +Fingerprints are built iteratively using components of the exception event. The flowchart below shows how fingerprints are generated. + +flowchart LR A\[Add exception type to fingerprint\] --> B{Stack trace
available?} B -->|No| C\[Add error message to fingerprint\] C --> D\[Final fingerprint\] B -->|Yes| E{In-app frames
exist?} E -->|No| F\[Add first frame to fingerprint\] E -->|Yes| G\[Add in-app frame to fingerprint
Priority: resolved > unresolved\] F --> D G --> D + +The flowchart in text + +Fingerprints are generated by considering the following in combination: + +1. The exception type +2. If there's no resolved stack trace, add the error message to the fingerprint +3. If there are stack traces but no in-app frames (frames from your code, not a dependency), use the first frame of the stack trace +4. If there are stack traces, in-app frames, and source maps available, use the resolved in-app stack frames +5. If there are stack traces, in-app frames, and source maps *not* available, use the first in-app stack frame + +In some languages, like Python, one error can trigger another, creating a chain of linked exceptions. PostHog records the entire chain in the event and generates a single fingerprint for it. + +### Ensuring accurate fingerprints + +[Resolved stack traces](/docs/error-tracking/stack-traces.md) are critical for accurate fingerprinting. Without accurate stack traces, PostHog cannot group exceptions consistently. If you have not uploaded source maps, follow the [source map guide](/docs/error-tracking/upload-source-maps.md) to do so. + +This also means that if the exception **type** or **message** changes from one version to the next, the fingerprint will change. + +## When are generated fingerprints used? + +Fingerprints are used to group similar exceptions into issues automatically. Automatic issue grouping is only done when: + +- No [issue grouping rules](/docs/error-tracking/grouping-issues.md) are applied +- No [issue merging](/docs/error-tracking/managing-issues.md#merging-issues) has been configured +- No [custom fingerprint](#customizing-fingerprints) is set during capture + +You can find details about how issue grouping works in the [issues and exceptions](/docs/error-tracking/issues-and-exceptions.md) guide. + +## Customizing fingerprints + +Fingerprints can be manually set during exception capture. This is a very useful way to group exceptions that are not related to each other. You can find examples of how to do this in the [custom issue grouping](/docs/error-tracking/grouping-issues.md#option-2-client-side-fingerprint) section. + +You can also learn more about grouping issues using rules in the [grouping issues](/docs/error-tracking/grouping-issues.md) guide. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/monitoring.md b/skills/posthog/all/skills/error-tracking-wordpress/references/monitoring.md new file mode 100644 index 00000000..6590cab2 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/monitoring.md @@ -0,0 +1,146 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Monitor and search issues - Docs + +Copy page + +# Monitor and search issues - Docs + +This guide covers how to find the most relevant, urgent, and impactful issues in your error tracking using the [issues page](https://app.posthog.com/error_tracking). + +## Monitoring issues + +When you're monitoring issues in your project, there are generally two common workflows: + +- You're exploring issues to identify impactful and problematic areas. You should use sorting features. +- You're looking for issues assigned to you to resolve them. You should filter by the `Assigned to` property. + +### Sorting issues + +Issues can be sorted by the following properties: + +| Property | Description | +| --- | --- | +| Last seen | The issue that has the most recent exception | +| First seen | The issue that has the oldest exception | +| Occurrences | The number of exceptions in the issue | +| Users | The number of unique users affected by the issue | +| Sessions | The number of unique sessions affected by the issue | + +Sorting by **last seen** and **occurrences** are great ways to get a general sense of issues in your project. Sorting by **users** and **sessions** is great to find the most impactful issues if you're using other [filters](#finding-specific-issues) to narrow down your results. + +### Monitoring issues assigned to you + +You can filter issues by the **Assigned to** property to find issues assigned to you. This is especially useful if you configure [automatic issue assignment](/docs/error-tracking/assigning-issues.md) and configure [alerts](/docs/error-tracking/alerts.md) to notify you when new issues are created. + +## Finding specific issues + +You can use the search bar at the top of the [issue page](https://app.posthog.com/error_tracking) to filter issues based on the properties of the exceptions in that issue. + +Search results are matched based on [properties of exception events](/docs/error-tracking/issues-and-exceptions.md) grouped into the issues. For example, if you search for "TypeError", we show you all issues where *any* exception grouped into the issue has a type of "TypeError". + +**Unrelated results** + +You may see seemingly unrelated issues in your search results because your search term matches an exception in the issue group. For example, you may see an issues named `RefreshError` when searching "schema", because a `get_schema` method appears on the exception stack traces. + +### Filtering modes + +The search bar provides two modes of filtering: + +#### 1\. Exact property filtering + +This operates like property filters elsewhere in PostHog, enabling you to add terms like `where 'http_referer' is set` or `where 'library' equals 'web'`. You add a property filter by clicking the property name shown here: + +![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_06_35_277_Z_54ad9274ba.png)![Adding a property to the property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_07_25_246_Z_709bdb93ad.png) + +Added property filters look like this: + +![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_41_220_Z_ac7ad6c492.png)![Search bar with property filter](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_08_14_625_Z_6c0ba08732.png) + +The results of both of these filter types (property filters and freeform search) are combined with `AND` logic, such that only exceptions that match all filters are included in the search results. + +#### 2\. Freeform text search + +This does text matching for a subset of the error tracking specific properties of the exception event. It splits the text you give it into tokens. The search matches an exception if *each* of the tokens in your search term appear in one of the following: + +- The exception type +- The exception message +- The function names in the exception stack trace (if known) +- The file paths in the exception stack trace (if known) + +For example, imagine you have an exception that looks like this: + +PostHog AI + +``` +TypeError: Cannot read property 'name' of undefined + at Object. (/path/to/myfile.js:123:45) + at Module._compile (module.js:653:30) + at Object.Module._extensions..js (module.js:664:10) + at Module.load (module.js:566:32) + at tryModuleLoad (module.js:506:12) + at Function.Module._load (module.js:498:3) + at Function.Module.runMain (module.js:694:10) + at startup (bootstrap_node.js:204:16) + at bootstrap_node.js:625:3 +``` + +If you search for the term `TypeError myfile.js`, the exception matches this search, as it contains `TypeError` (as the exception type) and `myfile.js` (as a file path in the stack trace). + +If you search for `TypeError myfile.js abc`, the exception would not match, as the token `abc` does not appear anywhere in freeform search properties. + +If you want to search for longer exact strings, e.g. a particular exception message, you can group tokens into a single term using quotes, e.g. `"Cannot read property 'name' of undefined" myfile.js` would match, and `"Cannot read property of myfile.js"` would not. + +Note, perhaps unintuitively, `Cannot read property of myfile.js` would match, because the tokens are ungrouped, and all of them appear *somewhere* in the exception search properties. + +### Searching chained exceptions + +Exception events can have more than one exception in them, due to language features like exception chaining. For freeform search, we put the types, messages, functions and file paths of all exceptions into one list, and match if the token appears in any of them. + +For example, if you had a chained exception with the messages `MyCustomError: Failed to load user` and `Cannot read property 'age' of undefined`, searching for `cannot read property` would match the exception, because it matches *one* of the exception messages (`property` appears in the "root" one). + +## Issue details + +When you click on an issue, you'll see the details page of the issue. + +This page shows you the following: + +- The stack trace, properties, and sessions related to the **currently selected exception**. +- Name, description, status, assignee, and external tracking links for the issue. +- A filterable list of all exceptions in the issue. **Selecting an exception** will show you the stack trace, properties, and sessions related to that exception at the top of the page. + +![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_50_11_322_Z_dfe9b9dd79.png)![An issue, with an unfiltered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T13_49_48_664_Z_30d13a2ef1.png) + +### Filtering exception occurrences within an issue + +Once you've found and opened the issue you want to investigate, you can use the same search interface to filter the exception list for a particular instance of the issue. This is particularly useful in cases where some exceptions in the issue have information others don't and you want to use that information for debugging. + +For example, you can add a property filter on `http_referer` that shows all exceptions where the `http_referer` is set: + +![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_45_290_Z_bf3b371db8.png)![An issue, with a filtered exception list](https://res.cloudinary.com/dmukukwp6/image/upload/pasted_image_2026_06_24_T10_11_28_842_Z_fe608ddf0a.png) + +**Alerts** + +If you have a set of filters that you use often, you can create alerts for them. This way you can be notified when new issues match your filters. Learn more about [alerts](/docs/error-tracking/alerts.md). + +## Improving search performance + +We try to return results to you within a second, but sometimes if you're querying over large amounts of data, it may take longer. The following can improve the search performance: + +- **Limit the time range you're searching over:** 7 days is usually enough to get a sense for the trends of an issue over time. + +- **Use freeform search rather than property filters:** Our freeform searches are generally faster than property filters, as the total amount of data processed is smaller. + +If you find your queries timing out or taking more than 30 seconds, please [let us know in-app](https://app.posthog.com/#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue)! We're always looking for benchmarks to improve against. + +## Suppressing issues + +If you find issues that are not useful to you, you can suppress them by changing the status to **Suppressed**. We recommend that you also implement [client-side suppression](/docs/error-tracking/capture.md#suppressing-exceptions) to not capture these exceptions in the first place, for cost and performance reasons. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/php.md b/skills/posthog/all/skills/error-tracking-wordpress/references/php.md new file mode 100644 index 00000000..7a3242cd --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/php.md @@ -0,0 +1,228 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Error Tracking installation - Docs + +Copy page + +# PHP Error Tracking installation - Docs + +1. 1 + + ## Install the PHP SDK + + Required + + Install the [PostHog PHP SDK](/docs/libraries/php.md) via Composer: + + Terminal + + PostHog AI + + ```bash + composer require posthog/posthog-php + ``` + +2. 2 + + ## Initialize the client + + Required + + Set your project token and instance address before making any calls: + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + ['host' => 'https://us.i.posthog.com'] + ); + ``` + + You can find your project token and instance address in the [project settings](https://app.posthog.com/settings/project) page in PostHog. + +3. 3 + + ## Capture exceptions + + Required + + Use `captureException` to manually capture exceptions and send them to PostHog as `$exception` events with full stack traces. + + ### Basic usage + + PHP + + PostHog AI + + ```php + try { + // Your code that might throw + riskyOperation(); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id'); + } + ``` + + ### With additional properties + + You can pass extra properties to include with the exception event: + + PHP + + PostHog AI + + ```php + try { + processOrder($orderId); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'user_distinct_id', [ + 'order_id' => $orderId, + 'environment' => 'production', + ]); + } + ``` + + You can also pass a plain string if you want to send an error message without a `Throwable`. + +4. 4 + + ## Enable automatic capture + + Recommended + + Automatic capture is opt-in for PHP. When enabled, the SDK installs handlers for uncaught exceptions. With the default `capture_errors: true`, it also captures PHP errors and fatal shutdown errors. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + ``` + + **Existing handlers are preserved** + + The SDK chains existing exception and error handlers instead of replacing your app's behavior. + +5. 5 + + ## Identify users and attach request context + + Recommended + + By default, automatically captured errors are anonymous. Use `context_provider` to attach a `distinctId` and request metadata to every automatically captured error event. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [ + '$current_url' => $_SERVER['REQUEST_URI'] ?? null, + '$request_method' => $_SERVER['REQUEST_METHOD'] ?? null, + '$exception_source' => $payload['source'] ?? null, + ], + ]; + }, + ], + ] + ); + ``` + + If `distinctId` is omitted, PostHog sends the event with an auto-generated ID and sets `$process_person_profile` to `false`. + +6. 6 + + ## Configure error tracking options + + Optional + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + 'capture_errors' => true, + 'excluded_exceptions' => [ + \InvalidArgumentException::class, + ], + 'max_frames' => 20, + 'context_provider' => static function (array $payload): array { + return [ + 'distinctId' => $_SESSION['user_id'] ?? null, + 'properties' => [], + ]; + }, + ], + ] + ); + ``` + + | Option | Type | Default | Description | + | --- | --- | --- | --- | + | enabled | boolean | false | Enables automatic error tracking handlers. Manual captureException works regardless. | + | capture_errors | boolean | true | When enabled, also captures PHP errors and fatal shutdown errors in addition to uncaught exceptions. | + | excluded_exceptions | array of class strings | [] | Throwable classes to skip during automatic capture. | + | max_frames | integer | 20 | Maximum number of stack frames included in $exception_list. | + | context_provider | callable or null | null | Callback that returns distinctId and extra event properties for automatic captures. | + +7. ## Verify error tracking + + Recommended + + Trigger a test exception to confirm events are being sent to PostHog. You should see them appear in the [Error Tracking](https://app.posthog.com/error_tracking) tab. + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + [ + 'host' => 'https://us.i.posthog.com', + 'error_tracking' => [ + 'enabled' => true, + ], + ] + ); + try { + throw new \Exception('Test exception from PHP'); + } catch (\Throwable $e) { + PostHog\PostHog::captureException($e, 'test_user'); + } + ``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/upload-source-maps.md b/skills/posthog/all/skills/error-tracking-wordpress/references/upload-source-maps.md new file mode 100644 index 00000000..b6ae318f --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/upload-source-maps.md @@ -0,0 +1,67 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Upload source maps - Docs + +Copy page + +# Upload source maps - Docs + +If you serve compiled or minified code, PostHog requires source maps to generate accurate stack traces. + +If your source maps are not publicly hosted, you will need to upload them during your build process to see unminified code in your stack traces. + +## AI wizard + +If you're using a JavaScript or TypeScript framework, set up source map uploading automatically with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt): + +`npx @posthog/wizard upload-source-maps` + +[Learn more](/wizard.md) + +Otherwise, choose your platform below for manual instructions. + +## Platforms + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/js.svg)Web](/docs/error-tracking/upload-source-maps/web.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nextjs.svg)Next.js](/docs/error-tracking/upload-source-maps/nextjs.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/nodejs.svg)Node.js](/docs/error-tracking/upload-source-maps/node.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React](/docs/error-tracking/upload-source-maps/react.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/docs/integrate/frameworks/angular.svg)Angular](/docs/error-tracking/upload-source-maps/angular.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/frameworks/nuxt.svg)Nuxt](/docs/error-tracking/upload-source-maps/nuxt.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/error-tracking/upload-source-maps/react-native.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Android_robot_bec2fb7318.svg)Android](/docs/error-tracking/upload-mappings/android.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/flutter.svg)Flutter](/docs/error-tracking/upload-source-maps/flutter.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/go.svg)Go](/docs/error-tracking/upload-source-maps/go.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/error-tracking/upload-source-maps/ios.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/kmp.svg)Kotlin Multiplatform](/docs/error-tracking/upload-debug-symbols/kmp.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/rust.svg)Rust](/docs/error-tracking/upload-source-maps/rust.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Rollup_js_c306a2fde3.svg)Rollup](/docs/error-tracking/upload-source-maps/rollup.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/webpack_3fc774b5a5.svg)Webpack](/docs/error-tracking/upload-source-maps/webpack.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/Vitejs_logo_98ffe5d5ee.svg)Vite](/docs/error-tracking/upload-source-maps/vite.md) + +- [CLI](/docs/error-tracking/upload-source-maps/cli.md) + +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/github_mark_903e35d471.svg)GitHub Action](/docs/error-tracking/upload-source-maps/github-actions.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/error-tracking-wordpress/references/wordpress.md b/skills/posthog/all/skills/error-tracking-wordpress/references/wordpress.md new file mode 100644 index 00000000..660f3189 --- /dev/null +++ b/skills/posthog/all/skills/error-tracking-wordpress/references/wordpress.md @@ -0,0 +1,109 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# How to set up WordPress analytics with PostHog - Docs + +Copy page + +# How to set up WordPress analytics with PostHog - Docs + +Getting traffic, usage, and user behavior data about your [WordPress](https://www.wordpress.org/) site is simple with PostHog. Once you have that data, you can discover insights and build dashboards with our suite of dev tools. + +## How to add PostHog to your WordPress site + +The best way to add PostHog to your WordPress site depends on what version of WordPress you are using. + +All of them require you to [signup for PostHog](https://us.posthog.com/signup), get your [snippet](/docs/getting-started/install?tab=snippet.md) with your project token and instance address from [your project settings](https://us.posthog.com/project/settings#snippet), and add the PostHog snippet to your site. + +### Option 1: Use a plugin + +The first option is to use a plugin. These enable you to easily add custom code to your site's header which we can use to add the PostHog snippet. + +For **WordPress.com** users, this is also the only option. This is because you don't have access to the `header.php` or `functions.php` files. Using plugins does require their **Business** or **Commerce** plans. We also recommend this option for [WooCommerce](/docs/libraries/woocommerce.md) sites. + +Two plugin options include: + +1. WordPress.com recommends using the free [Insert Headers and Footers](https://wordpress.com/plugins/insert-headers-and-footers) plugin. + +2. If you are already using Google Tag Manager on your WordPress site with a plugin like [Site Kit](https://wordpress.org/plugins/google-site-kit/), you can add the PostHog snippet as a tag instead. See our [Google Tag Manager docs](/docs/libraries/google-tag-manager.md) for more information. + +The workflow for these is the same: + +1. Install the plugin. +2. Add the PostHog snippet to the header via the plugin. +3. Activate the plugin. + +### Option 2: Edit your theme's functions file + +[Theme functions](https://developer.wordpress.org/themes/basics/theme-functions/) enable you to add functionality to your WordPress site. This makes them a great way to add PostHog. + +To set one up for PostHog, first, find your theme's `functions.php` file. This can be found either in `app/public/wp-content/themes/` folder or in your WordPress admin under **Tools** -> **Theme Filter Editor**. + +Next, create an `add_posthog` function with your snippet like this: + +PHP + +PostHog AI + +```php +if ( ! function_exists( 'add_posthog' ) ) : +function add_posthog() { + ?> + + + + tag +add_action('wp_head', 'add_posthog', 999); +``` + +After saving your changes or clicking **Update File**, PostHog should begin to autocapture pageviews, clicks, and more. + +### Option 3: Edit your theme's header file + +If you are using an older version of WordPress, you can edit the `header.php` file directly. + +To do this, start by going to your WordPress admin and navigating to **Appearance** -> **Theme Editor**. + +Select your theme in the editor drop-down menu to the right and click the `header.php` file in the file column to the right. + +You should now see the contents of the `header.php` template file in the code editing view. It is recommended that you copy all the text/code and save it somewhere as a back-up. + +Find the closing `` in the code editor and paste the PostHog snippet before it (see above image). Finally, click the **Update File** button at the bottom to save your changes. PostHog should begin to autocapture pageviews, clicks, and more. + +To confirm PostHog is configured correctly, visit your website and then check if the events from your session appear in PostHog. + +> **Notes:** +> +> - Using the Theme Editor is very convenient, but you have to consider the potential drawbacks of having template files writable, which many prefer to disable for security purposes. Also, wrongfully editing a file may cause problems so be sure to perform appropriate backups before attempting this. +> - If your theme auto-updates, manually editing the `header.php` file may lose your settings. Making a [Child Theme](https://developer.wordpress.org/themes/advanced-topics/child-themes/) is the recommended approach. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-android/SKILL.md b/skills/posthog/all/skills/feature-flags-android/SKILL.md index 9c78521c..fcb93b62 100644 --- a/skills/posthog/all/skills/feature-flags-android/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-android/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-android description: PostHog feature flags for Android applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Android @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Android applications. - `references/android.md` - Android feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version - Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. - Initialize PostHog in the Application class's `onCreate()` method diff --git a/skills/posthog/all/skills/feature-flags-android/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-android/references/COMMANDMENTS.md new file mode 100644 index 00000000..c18de5d4 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-android/references/COMMANDMENTS.md @@ -0,0 +1,9 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. diff --git a/skills/posthog/all/skills/feature-flags-android/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-android/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-android/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-android/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-android/references/android.md b/skills/posthog/all/skills/feature-flags-android/references/android.md index 05dfcfe5..9c596f65 100644 --- a/skills/posthog/all/skills/feature-flags-android/references/android.md +++ b/skills/posthog/all/skills/feature-flags-android/references/android.md @@ -1,4 +1,10 @@ -# Android feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Android Feature Flags installation - Docs + +Copy page + +# Android Feature Flags installation - Docs 1. 1 @@ -88,7 +94,7 @@ if (isMyFlagEnabled) { // Do something differently for this user // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + val matchedFlagPayload = PostHog.getFeatureFlagResult("flag-key")?.payload } ``` @@ -109,7 +115,7 @@ if (enabledVariant == "variant-key") { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + val matchedFlagPayload = PostHog.getFeatureFlagResult("flag-key")?.payload } ``` @@ -137,9 +143,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-android/references/best-practices.md b/skills/posthog/all/skills/feature-flags-android/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-android/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-android/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-api/SKILL.md b/skills/posthog/all/skills/feature-flags-api/SKILL.md index 3bf62549..53bde1f1 100644 --- a/skills/posthog/all/skills/feature-flags-api/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-api/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-api description: PostHog feature flags for API applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for API @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to API applications. - `references/api.md` - API feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,4 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op diff --git a/skills/posthog/all/skills/feature-flags-api/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-api/references/COMMANDMENTS.md new file mode 100644 index 00000000..08d1eb78 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-api/references/COMMANDMENTS.md @@ -0,0 +1,5 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op diff --git a/skills/posthog/all/skills/feature-flags-api/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-api/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-api/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-api/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-api/references/api.md b/skills/posthog/all/skills/feature-flags-api/references/api.md index 6c156b22..23b83580 100644 --- a/skills/posthog/all/skills/feature-flags-api/references/api.md +++ b/skills/posthog/all/skills/feature-flags-api/references/api.md @@ -1,4 +1,10 @@ -# API feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# API Feature Flags installation - Docs + +Copy page + +# API Feature Flags installation - Docs 1. 1 @@ -185,9 +191,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-api/references/best-practices.md b/skills/posthog/all/skills/feature-flags-api/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-api/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-api/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/SKILL.md b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/SKILL.md new file mode 100644 index 00000000..5ae4d181 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/SKILL.md @@ -0,0 +1,48 @@ +--- +name: feature-flags-aspnetcore-feature-management +description: >- + PostHog feature flags for ASP.NET Core applications using + Microsoft.FeatureManagement +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for ASP.NET Core Feature Management + +This skill helps you add PostHog feature flags to ASP.NET Core Feature Management applications. + +## Reference files + +- `references/dotnet.md` - .net feature flags installation - docs +- `references/dotnet.md` - .net - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- In ASP.NET Core apps, prefer `builder.AddPostHog()` from `PostHog.AspNetCore` and inject `IPostHogClient` from dependency injection instead of manually constructing clients in controllers +- Configure ASP.NET Core apps with the `PostHog` configuration section or environment variable fallbacks such as `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` +- Add product analytics captures at route, controller, or handler boundaries where meaningful user actions occur; do not track every low-level method call +- Capture request exceptions in middleware with `CaptureException` and then rethrow so existing ASP.NET Core error handling still runs +- For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet diff --git a/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/COMMANDMENTS.md new file mode 100644 index 00000000..352c74f5 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- In ASP.NET Core apps, prefer `builder.AddPostHog()` from `PostHog.AspNetCore` and inject `IPostHogClient` from dependency injection instead of manually constructing clients in controllers +- Configure ASP.NET Core apps with the `PostHog` configuration section or environment variable fallbacks such as `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` +- Add product analytics captures at route, controller, or handler boundaries where meaningful user actions occur; do not track every low-level method call +- Capture request exceptions in middleware with `CaptureException` and then rethrow so existing ASP.NET Core error handling still runs +- For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet diff --git a/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/best-practices.md b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/dotnet.md b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/dotnet.md new file mode 100644 index 00000000..23566f00 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-aspnetcore-feature-management/references/dotnet.md @@ -0,0 +1,773 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# .NET - Docs + +Copy page + +# .NET - Docs + +This is an optional library you can install if you're working with .NET Core. It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance. + +## Installation + +The `PostHog` package supports any .NET platform that targets .NET Standard 2.1 or .NET 8+, including MAUI, Blazor, and console applications. The `PostHog.AspNetCore` package provides additional conveniences for ASP.NET Core applications such as streamlined registration, request-scoped caching, and integration with [.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference). + +> **Note:** We actively test with ASP.NET Core. Other platforms should work but haven't been specifically tested. If you encounter issues, please [report them on GitHub](https://github.com/PostHog/posthog-dotnet/issues). + +> **Not supported:** Classic UWP (requires .NET Standard 2.0 only). Microsoft has [deprecated UWP](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/migrate-to-windows-app-sdk-ovw) in favor of the Windows App SDK. For Unity projects, see our dedicated [Unity SDK](/docs/libraries/unity.md). + +Terminal + +PostHog AI + +```bash +dotnet add package PostHog.AspNetCore +``` + +In your `Program.cs` (or `Startup.cs` for ASP.NET Core 2.x) file, add the following code: + +C# + +PostHog AI + +```csharp +using PostHog; +var builder = WebApplication.CreateBuilder(args); +// Add PostHog to the dependency injection container as a singleton. +builder.AddPostHog(); +``` + +Make sure to configure PostHog with your project token, instance address, and optional personal API key. For example, in `appsettings.json`: + +JSON + +PostHog AI + +```json +{ + "PostHog": { + "ProjectToken": "", + "HostUrl": "https://us.i.posthog.com" + } +} +``` + +> **Note:** If the host is not specified, the default host `https://us.i.posthog.com` is used. + +Use a secrets manager to store your personal API key. For example, when developing locally you can use the `UserSecrets` feature of the `dotnet` CLI: + +Terminal + +PostHog AI + +```bash +dotnet user-secrets init +dotnet user-secrets set "PostHog:PersonalApiKey" "phx_..." +``` + +You can find your project token and instance address in the [project settings](https://app.posthog.com/project/settings) page in PostHog. + +## Working with .NET Feature Management + +`PostHog.AspNetCore` supports [.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference). This enables you to use the tag helper and the `FeatureGateAttribute` in your ASP.NET Core applications to gate access to certain features using PostHog feature flags. + +To use feature flags with the .NET Feature Management library, you'll need to implement the `IPostHogFeatureFlagContextProvider` interface. The quickest way to do that is to inherit from the `PostHogFeatureFlagContextProvider` class and override the `GetDistinctId` and `GetFeatureFlagOptionsAsync` methods. + +C# + +PostHog AI + +```csharp +public class MyFeatureFlagContextProvider(IHttpContextAccessor httpContextAccessor) + : PostHogFeatureFlagContextProvider +{ + protected override string? GetDistinctId() + => httpContextAccessor.HttpContext?.User.Identity?.Name; + protected override ValueTask GetFeatureFlagOptionsAsync() + { + // In a real app, you might get this information from a + // database or other source for the current user. + return ValueTask.FromResult( + new FeatureFlagOptions + { + PersonProperties = new Dictionary + { + ["email"] = "some-test@example.com" + }, + OnlyEvaluateLocally = true + }); + } +} +``` + +Then, register your implementation in `Program.cs` (or `Startup.cs`): + +C# + +PostHog AI + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.AddPostHog(options => { + options.UseFeatureManagement(); +}); +``` + +With this in place, you can now use `feature` tag helpers in your Razor views: + +HTML + +PostHog AI + +```html + +

This is the new feature!

+
+ +

Sorry, no awesome new feature for you.

+
+``` + +Multivariate feature flags are also supported: + +HTML + +PostHog AI + +```html + +

This is the new feature variant A!

+
+ +

This is the new feature variant B!

+
+``` + +You can also use the `FeatureGateAttribute` to gate access to controllers or actions: + +C# + +PostHog AI + +```csharp +[FeatureGate("awesome-new-feature")] +public class NewFeatureController : Controller +{ + public IActionResult Index() + { + return View(); + } +} +``` + +## Using the core package without ASP.NET Core + +If you're not using ASP.NET Core (for example, in a console application, MAUI app, or Blazor WebAssembly), install the `PostHog` package instead of `PostHog.AspNetCore`. This package has no ASP.NET Core dependencies and can be used in any .NET project targeting .NET Standard 2.1 or .NET 8+. + +Terminal + +PostHog AI + +```bash +dotnet add package PostHog +``` + +The `PostHogClient` class must be implemented as a singleton in your project. For `PostHog.AspNetCore`, this is handled by the `builder.AddPostHog();` method. For the `PostHog` package, you can do the following if you're using dependency injection: + +C# + +PostHog AI + +```csharp +builder.Services.AddPostHog(); +``` + +If you're not using a `builder` (such as in a console application), you can do the following: + +C# + +PostHog AI + +```csharp +using PostHog; +var services = new ServiceCollection(); +services.AddPostHog(); +var serviceProvider = services.BuildServiceProvider(); +var posthog = serviceProvider.GetRequiredService(); +``` + +The `AddPostHog` methods accept an optional `Action` parameter that you can use to configure the client. + +If you're not using dependency injection, you can create a static instance of the `PostHogClient` class and use that everywhere in your project: + +C# + +PostHog AI + +```csharp +using PostHog; +public static readonly PostHogClient PostHog = new(new PostHogOptions { + ProjectToken = "", + HostUrl = new Uri("https://us.i.posthog.com"), + PersonalApiKey = Environment.GetEnvironmentVariable( + "PostHog__PersonalApiKey") +}); +``` + +## Debug mode + +If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. + +To see detailed logging, set the log level to `Debug` or `Trace` in `appsettings.json`: + +JSON + +PostHog AI + +```json +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "PostHog": "Trace" + } + }, + ... +} +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +## Capturing events + +You can send custom events using `capture`: + +C# + +PostHog AI + +```csharp +posthog.Capture("distinct_id_of_the_user", "user_signed_up"); +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_the_user", + "user_signed_up", + properties: new() { + ["login_type"] = "email", + ["is_free_trial"] = "true" + } +); +``` + +### Sending page views + +If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send `$pageview` events from your backend like so: + +C# + +PostHog AI + +```csharp +using PostHog; +using Microsoft.AspNetCore.Http.Extensions; +posthog.CapturePageView( + "distinct_id_of_the_user", + HttpContext.Request.GetDisplayUrl()); +``` + +## Request context + +For ASP.NET Core apps using `PostHog.AspNetCore`, add request context middleware before routes that call PostHog. This reads incoming PostHog tracing headers and attaches request metadata to captures, exceptions, and feature flag evaluation inside the request. + +Program.cs + +PostHog AI + +```csharp +using PostHog; +using PostHog.AspNetCore; +var builder = WebApplication.CreateBuilder(args); +builder.AddPostHog(); +var app = builder.Build(); +app.UsePostHogRequestContext(); +``` + +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your ASP.NET Core backend hostname so browser requests include the session and distinct ID headers. + +The middleware reads `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` as request-scoped analytics context. It also adds request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip`. Explicit distinct IDs and event properties always override request context. + +Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side decisions, pass an authenticated distinct ID explicitly. You can ignore tracing headers while still collecting request metadata: + +C# + +PostHog AI + +```csharp +app.UsePostHogRequestContext(options => +{ + options.UseTracingHeaders = false; +}); +``` + +Request-context overloads like `posthog.Capture("checkout started")` and `posthog.EvaluateFlagsAsync()` use the current request distinct ID when one is available. + +## Error tracking + +You can manually capture exceptions using `CaptureException`. This sends a `$exception` event with stack frames, inner exceptions, aggregate exceptions, source context when available, and .NET runtime metadata. + +File names, line numbers, and source context depend on debug information already available from the captured .NET stack trace. PostHog doesn't support uploading .NET PDB files yet, so production builds without runtime-accessible debug information may show less detailed stack frames. + +C# + +PostHog AI + +```csharp +try +{ + ProcessOrder(orderId); +} +catch (Exception exception) +{ + posthog.CaptureException(exception, "user_distinct_id"); +} +``` + +Add custom properties to include request, tenant, or domain context: + +C# + +PostHog AI + +```csharp +posthog.CaptureException( + exception, + "user_distinct_id", + new Dictionary + { + ["order_id"] = orderId, + ["environment"] = "production", + } +); +``` + +For the full setup guide, see the [.NET error tracking installation docs](/docs/error-tracking/installation/dotnet.md). + +Automatic exception capture is not available in the .NET SDK yet. + +## Logs + +[PostHog Logs](/docs/logs.md) doesn't use this SDK. Logs are ingested over OpenTelemetry, so you attach an OTLP exporter to the standard `ILogger` pipeline instead — see the [.NET logs installation guide](/docs/logs/installation/dotnet.md). + +## Person profiles and properties + +The .NET SDK captures identified events by default. These create [person profiles](/docs/data/persons.md). To set [person properties](/docs/data/user-properties.md) in these profiles, include them when capturing an event: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + personPropertiesToSet: new() { ["name"] = "Max Hedgehog" }, + personPropertiesToSetOnce: new() { ["initial_url"] = "/blog" } +); +``` + +For more details on the difference between `$set` and `$set_once`, see our [person properties docs](/docs/data/user-properties.md#what-is-the-difference-between-set-and-set_once). + +To capture [anonymous events](/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's `$process_person_profile` property to `false`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + properties: new() { + ["$process_person_profile"] = false + } +) +``` + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +C# + +PostHog AI + +```csharp +await posthog.AliasAsync("current_distinct_id", "new_distinct_id"); +``` + +We strongly recommend reading our docs on [alias](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Group analytics + +Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the [group analytics](/docs/product-analytics/group-analytics.md) guide for more information. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on our [pricing page](/pricing.md). + +To capture an event and associate it with a group, add the `groups` argument to your `Capture` call: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "user_distinct_id", + "some_event", + groups: [new Group("company", "company_id_in_your_db")]); +``` + +Update properties on a group, use the `GroupIdentifyAsync` method: + +C# + +PostHog AI + +```csharp +await posthog.GroupIdentifyAsync( + type: "company", + key: "company_id_in_your_db", + name: "Awesome Inc.", + properties: new() + { + ["employees"] = 11 + } +); +``` + +The `name` is a special property which is used in the PostHog UI for the name of the group. If you don't specify a `name` property, the group ID will be used instead. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Evaluation contexts + +Configure evaluation contexts so this SDK only evaluates flags intended for the matching application, platform, or product area. For ASP.NET Core apps using `PostHog.AspNetCore`, add them to the `PostHog` configuration section: + +JSON + +PostHog AI + +```json +{ + "PostHog": { + "ProjectToken": "", + "HostUrl": "https://us.i.posthog.com", + "EvaluationContexts": ["main-app", "api", "backend"] + } +} +``` + +For code-based configuration, set `EvaluationContexts` on `PostHogOptions`: + +C# + +PostHog AI + +```csharp +var posthog = new PostHogClient(new PostHogOptions +{ + ProjectToken = "", + HostUrl = new Uri("https://us.i.posthog.com"), + EvaluationContexts = ["main-app", "api", "backend"], +}); +``` + +Remote `/flags` requests from `EvaluateFlagsAsync()` include `evaluation_contexts` when configured. + +For more details, see the [evaluation contexts guide](/docs/feature-flags/evaluation-contexts.md). + +### Local evaluation + +Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. + +It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls. + +For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation.md). + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("user_distinct_id"); +var variant = flags.GetFlag("experiment-feature-flag-key")?.VariantKey; +if (variant == "variant-name") +{ + // Do something +} +``` + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## AI observability + +`PostHog.AI` adds [AI observability](/docs/ai-observability.md) for .NET applications using OpenAI or Azure OpenAI. It is currently pre-release, so expect breaking changes before a stable release. + +For installation instructions, see the [OpenAI guide for .NET](/docs/ai-observability/installation/openai.md#net-support) or the [Azure OpenAI guide for .NET](/docs/ai-observability/installation/azure-openai.md#net-support). + +## GeoIP properties + +The `posthog-dotnet` library disregards the server IP, does not add the GeoIP properties, and does not use the values for feature flag evaluations. + +## Serverless environments (Azure Functions/Render/Lambda/...) + +By default, the library buffers events before sending them to the `/batch` endpoint for better performance. This can lead to lost events in serverless environments if the .NET process is terminated by the platform before the buffer is fully flushed. + +To avoid this, call `await posthog.FlushAsync()` after processing every request by adding it as a middleware to your server. This allows `posthog.Capture()` to remain asynchronous for better performance. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-django/SKILL.md b/skills/posthog/all/skills/feature-flags-django/SKILL.md new file mode 100644 index 00000000..086a6fdb --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/SKILL.md @@ -0,0 +1,51 @@ +--- +name: feature-flags-django +description: PostHog feature flags for Django applications +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for Django + +This skill helps you add PostHog feature flags to Django applications. + +## Reference files + +- `references/python.md` - Python feature flags installation - docs +- `references/django.md` - Django - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions +- Initialize PostHog in AppConfig.ready() with api_key and host from environment variables +- The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own +- The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() +- Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/feature-flags-django/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-django/references/COMMANDMENTS.md new file mode 100644 index 00000000..ca143695 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions +- Initialize PostHog in AppConfig.ready() with api_key and host from environment variables +- The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own +- The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() +- Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/feature-flags-django/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-django/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-django/references/best-practices.md b/skills/posthog/all/skills/feature-flags-django/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-django/references/django.md b/skills/posthog/all/skills/feature-flags-django/references/django.md new file mode 100644 index 00000000..e143a17f --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/references/django.md @@ -0,0 +1,300 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Django - Docs + +Copy page + +# Django - Docs + +PostHog makes it easy to get data about traffic and usage of your Django app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more. + +This guide walks you through integrating PostHog into your Django app using the [Python SDK](/docs/libraries/python.md). + +## Beta: integration via LLM + +Install PostHog for Django in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. + +`npx @posthog/wizard` + +[Learn more](/wizard.md) + +Or, to integrate manually, continue with the rest of this guide. + +> These docs cover version `7.x` of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See [supported versions](#supported-versions). + +## Installation + +To start, run `pip install posthog` to install PostHog’s Python SDK. + +Then, configure PostHog in your app config so it's initialized when Django starts: + +your\_app/apps.py + +PostHog AI + +```python +from django.apps import AppConfig +import posthog +class YourAppConfig(AppConfig): + name = 'your_app_name' + def ready(self): + posthog.api_key = '' + posthog.host = 'https://us.i.posthog.com' +``` + +Next, if you haven't done so already, add your `AppConfig` to `INSTALLED_APPS` in `settings.py`: + +settings.py + +PostHog AI + +```python +INSTALLED_APPS = [ + # ... other apps + 'your_app_name.apps.YourAppConfig', +] +``` + +You can find your project token and instance address in [your project settings](https://app.posthog.com/project/settings). + +To capture events from any file, import `posthog` and call the method you need. For example: + +Python + +PostHog AI + +```python +import posthog +from posthog import identify_context +def some_request(request): + with posthog.new_context(): + # Django includes request.user for anonymous visitors too. Only identify + # the context when the visitor is logged in. + if request.user.is_authenticated: + identify_context(str(request.user.pk)) + posthog.capture('event_name') +``` + +Events captured without a context or explicit `distinct_id` are sent as [anonymous events](/docs/data/anonymous-vs-identified-events.md) with an auto-generated `distinct_id`. See the [Python SDK docs](/docs/libraries/python.md#person-profiles-and-properties) for more details. + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` to associate events with the correct user. +> +> In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct `distinct_id`. Typically, you would set a fresh context and identify at the top of each route. +> +> Python +> +> PostHog AI +> +> ```python +> from posthog import new_context, identify_context, capture +> @app.get("/foo") +> def foo(current_user: User = Depends(get_current_user)): +> with new_context(): # Set context at the top of a route +> identify_context(current_user.id) +> capture("foo_viewed") +> return {"status": "ok"} +> ``` +> +> When possible, write a small piece of **middleware** that resolves your authenticated user, wrap a context around the request, and identifies it. Every `capture()` downstream is then attributed *automatically*. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK. + +## Django contexts middleware + +The Python SDK provides a Django middleware that automatically wraps all requests with a [context](/docs/libraries/python.md#contexts). This middleware extracts session and user information from each request and tags all events captured during that request with relevant metadata. + +### Basic setup + +Add the middleware to your Django settings. If your app uses Django authentication, place it after `django.contrib.auth.middleware.AuthenticationMiddleware` so the middleware can use the authenticated Django user as a distinct ID fallback and capture the user's email. + +Python + +PostHog AI + +```python +MIDDLEWARE = [ + # ... other middleware + 'posthog.integrations.django.PosthogContextMiddleware', + # ... other middleware +] +``` + +The middleware uses the globally configured `posthog` client by default, so you don't need to create or pass it a separate client instance. + +The middleware automatically extracts and uses: + +- **Session ID** from the `X-POSTHOG-SESSION-ID` header, if present +- **Distinct ID** from the `X-POSTHOG-DISTINCT-ID` header, if present, falling back to the authenticated Django user's `pk` (Django's primary-key alias, which works with custom user models) +- **User email** from the authenticated Django user's `email` as `email` +- **Current URL** as `$current_url` +- **Request method** as `$request_method` +- **Request path** as `$request_path` +- **Forwarded IP address** from `X-Forwarded-For` as `$ip` +- **User agent** from `User-Agent` as `$user_agent` + +The session and distinct ID headers are sanitized before use. Empty values are ignored, control characters are removed, values are trimmed, and values are capped at 1000 characters. + +All events captured during the request (including exceptions) include these properties and are associated with the extracted session and distinct ID. + +### Login and signup views + +The middleware reads `request.user` once, before your view runs. On a login or signup request the visitor is still anonymous at that point, so the request's context has no distinct ID. Calling `login()` inside the view doesn't change that. Everything captured during that request stays anonymous, including the login event itself. + +Identify the context from inside the request once you know who the user is. Django's auth signals are the natural place: + +Python + +PostHog AI + +```python +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver +from posthog import identify_context +@receiver(user_logged_in) +def identify_posthog_user(sender, request, user, **kwargs): + identify_context(str(user.pk)) +``` + +Every capture later in that request is then attributed to the user who just logged in. Requests made after login don't need this. The middleware sees the authenticated user from the start. + +If you're using [PostHog JavaScript Web](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Django backend hostname so browser requests include the session and distinct ID headers. + +### Exception capture + +By default, the middleware captures exceptions and sends them to PostHog's error tracking using the globally configured `posthog` client. This includes Django view exceptions that Django converts into error responses. + +Disable this by setting: + +Python + +PostHog AI + +```python +# settings.py +POSTHOG_MW_CAPTURE_EXCEPTIONS = False +``` + +### Adding custom tags + +Use `POSTHOG_MW_EXTRA_TAGS` to add custom properties to all requests: + +Python + +PostHog AI + +```python +# settings.py +def add_user_tags(request): + # type: (HttpRequest) -> Dict[str, Any] + tags = {} + if hasattr(request, 'user') and request.user.is_authenticated: + # Use pk instead of id so this works with custom User primary keys. + tags['user_id'] = str(request.user.pk) + tags['email'] = request.user.email + return tags +POSTHOG_MW_EXTRA_TAGS = add_user_tags +``` + +#### Filtering requests + +Skip tracking for certain requests using `POSTHOG_MW_REQUEST_FILTER`: + +Python + +PostHog AI + +```python +# settings.py +def should_track_request(request): + # type: (HttpRequest) -> bool + # Don't track health checks or admin requests + if request.path.startswith('/health') or request.path.startswith('/admin'): + return False + return True +POSTHOG_MW_REQUEST_FILTER = should_track_request +``` + +### Modifying default tags + +Use `POSTHOG_MW_TAG_MAP` to modify or remove default tags: + +Python + +PostHog AI + +```python +# settings.py +def customize_tags(tags): + # type: (Dict[str, Any]) -> Dict[str, Any] + # Remove URL for privacy + tags.pop('$current_url', None) + # Add custom prefix to method + if '$request_method' in tags: + tags['http_method'] = tags.pop('$request_method') + return tags +POSTHOG_MW_TAG_MAP = customize_tags +``` + +### Complete configuration example + +Python + +PostHog AI + +```python +# settings.py +def add_request_context(request): + # type: (HttpRequest) -> Dict[str, Any] + tags = {} + if hasattr(request, 'user') and request.user.is_authenticated: + tags['user_type'] = 'authenticated' + # Use pk instead of id so this works with custom User primary keys. + tags['user_id'] = str(request.user.pk) + else: + tags['user_type'] = 'anonymous' + # Add request info + tags['user_agent'] = request.META.get('HTTP_USER_AGENT', '') + return tags +def filter_tracking(request): + # type: (HttpRequest) -> bool + # Skip internal endpoints + return not request.path.startswith(('/health', '/metrics', '/admin')) +def clean_tags(tags): + # type: (Dict[str, Any]) -> Dict[str, Any] + # Remove sensitive data + tags.pop('user_agent', None) + return tags +POSTHOG_MW_EXTRA_TAGS = add_request_context +POSTHOG_MW_REQUEST_FILTER = filter_tracking +POSTHOG_MW_TAG_MAP = clean_tags +POSTHOG_MW_CAPTURE_EXCEPTIONS = True +``` + +All events captured within the request context automatically include the configured tags and are associated with the session and user identified from the request headers or Django authentication. + +The middleware supports both sync (WSGI) and async (ASGI) Django applications. In async mode, it uses Django's `request.auser()` API when available to avoid synchronous user access. + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Django (such as analytics, feature flags, A/B testing, etc.), have a look at our [Python SDK docs](/docs/libraries/python.md). + +Alternatively, the following tutorials can help you get started: + +- [Setting up Django analytics, feature flags, and more](/tutorials/django-analytics.md) +- [How to set up A/B tests in Django](/tutorials/django-ab-tests.md) + +## Supported versions + +These docs cover version `7.x` of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on `7.x.x` and higher — pin to the 6.x line with `pip install 'posthog<7'`, where `6.9.3` is the final release. + +Everything on this page works the same way on `6.9.3`. Event capture, the context API (`new_context`, `identify_context`, `set_context_session`), and `PosthogContextMiddleware` are identical on `6.9.3` and `7.0.0` — `7.0.0` only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the `X-POSTHOG-DISTINCT-ID` header and falling back to the authenticated user, which behaves the same across both lines. + +Later `7.x` releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and `set_context_device_id`. They also changed the middleware's own captured properties: `7.x` sends the request IP as `$ip`, where `6.9.3` sends it as `$ip_address`, and `7.x` additionally captures `$request_path`, `$raw_user_agent`, and the authenticated user's `email`. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-django/references/python.md b/skills/posthog/all/skills/feature-flags-django/references/python.md new file mode 100644 index 00000000..30634ac5 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-django/references/python.md @@ -0,0 +1,197 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Feature Flags installation - Docs + +Copy page + +# Python Feature Flags installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog Python library using pip: + + Terminal + + PostHog AI + + ```bash + pip install posthog + ``` + +2. 2 + + ## Initialize PostHog + + Required + + Initialize the PostHog client with your project token and host from your project settings: + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog( + project_api_key='', + host='https://us.i.posthog.com' + ) + ``` + + **Django integration** + + If you're using Django, check out our [Django integration](/docs/libraries/django.md) for automatic request tracking. + +3. 3 + + ## Send events + + Recommended + + Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration: + + Capture custom events by calling the `capture` method with an event name and properties: + + Python + + PostHog AI + + ```python + import posthog + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) + ``` + +4. 4 + + ## Evaluate boolean feature flags + + Required + + Check if a feature flag is enabled: + + ```python + is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') + if is_my_flag_enabled: + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + ``` + +5. 5 + + ## Evaluate multivariate feature flags + + Optional + + For multivariate flags, check which variant the user has been assigned: + + ```python + enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') + if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + ``` + +6. 6 + + ## Include feature flag information in events + + Required + + If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + + **Note:** This step is only required for events captured using our server-side SDKs or API. + + ## Set send_feature_flags (recommended) + + Set `send_feature_flags` to `True` in your capture call: + + Python + + PostHog AI + + ```python + posthog.capture( + distinct_id="distinct_id_of_the_user", + event='event_name', + send_feature_flags=True + ) + ``` + + ## Include $feature property + + Include the `$feature/feature_flag_name` property in your event properties: + + Python + + PostHog AI + + ```python + posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + }, + ) + ``` + +7. 7 + + ## Override server properties + + Optional + + Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with: + + ```python + posthog.get_feature_flag( + 'flag-key', + 'distinct_id_of_the_user', + person_properties={'property_name': 'value'}, + groups={ + 'your_group_type': 'your_group_id', + 'another_group_type': 'your_group_id'}, + group_properties={ + 'your_group_type': {'group_property_name': 'value'}, + 'another_group_type': {'group_property_name': 'value'} + }, + ) + ``` + +8. 8 + + ## Running experiments + + Optional + + Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard. + +9. 9 + + ## Next steps + + Recommended + + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. + + | Resource | Description | + | --- | --- | + | [Creating a feature flag](/docs/feature-flags/creating-feature-flags.md) | How to create a feature flag in PostHog | + | [Adding feature flag code](/docs/feature-flags/adding-feature-flag-code.md) | How to check flags in your code for all platforms | + | [Framework-specific guides](/docs/feature-flags/tutorials.md#framework-guides) | Setup guides for React Native, Next.js, Flutter, and other frameworks | + | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | + | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-dotnet/SKILL.md b/skills/posthog/all/skills/feature-flags-dotnet/SKILL.md index b509c0a9..d432b405 100644 --- a/skills/posthog/all/skills/feature-flags-dotnet/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-dotnet/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-dotnet description: PostHog feature flags for .NET applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for .NET @@ -13,8 +13,10 @@ This skill helps you add PostHog feature flags to .NET applications. ## Reference files - `references/dotnet.md` - .net feature flags installation - docs +- `references/dotnet.md` - .net - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +33,9 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet diff --git a/skills/posthog/all/skills/feature-flags-dotnet/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-dotnet/references/COMMANDMENTS.md new file mode 100644 index 00000000..d726cbc9 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-dotnet/references/COMMANDMENTS.md @@ -0,0 +1,10 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-dotnet package names are `PostHog` for general .NET apps and `PostHog.AspNetCore` for ASP.NET Core apps +- Use environment variables, user secrets, or configuration providers for `ProjectToken`, `HostUrl`, and `PersonalApiKey`; never hardcode PostHog secrets +- For CLIs, scripts, workers, and other short-lived processes, create one `PostHogClient` for the process lifetime and call `FlushAsync()` before exit +- Call `IdentifyAsync` for known users and put PII such as email in person properties, not in event properties +- Use `CaptureException(exception, distinctId, properties, groups, flags)` for handled exceptions; automatic exception capture is not available in the .NET SDK yet diff --git a/skills/posthog/all/skills/feature-flags-dotnet/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-dotnet/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-dotnet/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-dotnet/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-dotnet/references/best-practices.md b/skills/posthog/all/skills/feature-flags-dotnet/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-dotnet/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-dotnet/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-dotnet/references/dotnet.md b/skills/posthog/all/skills/feature-flags-dotnet/references/dotnet.md index ad5b69f4..23566f00 100644 --- a/skills/posthog/all/skills/feature-flags-dotnet/references/dotnet.md +++ b/skills/posthog/all/skills/feature-flags-dotnet/references/dotnet.md @@ -1,10 +1,20 @@ -# .NET feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# .NET - Docs + +Copy page + +# .NET - Docs + +This is an optional library you can install if you're working with .NET Core. It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance. + +## Installation The `PostHog` package supports any .NET platform that targets .NET Standard 2.1 or .NET 8+, including MAUI, Blazor, and console applications. The `PostHog.AspNetCore` package provides additional conveniences for ASP.NET Core applications such as streamlined registration, request-scoped caching, and integration with [.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference). > **Note:** We actively test with ASP.NET Core. Other platforms should work but haven't been specifically tested. If you encounter issues, please [report them on GitHub](https://github.com/PostHog/posthog-dotnet/issues). -> **Not supported:** Classic UWP (requires .NET Standard 2.0 only). Microsoft has [deprecated UWP](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/migrate-to-windows-app-sdk-ovw) in favor of the Windows App SDK. For Unity projects, see our dedicated [Unity SDK](/docs/libraries/unity.md) (currently in beta). +> **Not supported:** Classic UWP (requires .NET Standard 2.0 only). Microsoft has [deprecated UWP](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/migrate-to-windows-app-sdk-ovw) in favor of the Windows App SDK. For Unity projects, see our dedicated [Unity SDK](/docs/libraries/unity.md). Terminal @@ -36,7 +46,7 @@ PostHog AI ```json { "PostHog": { - "ProjectApiKey": "", + "ProjectToken": "", "HostUrl": "https://us.i.posthog.com" } } @@ -197,7 +207,7 @@ PostHog AI ```csharp using PostHog; public static readonly PostHogClient PostHog = new(new PostHogOptions { - ProjectApiKey = "", + ProjectToken = "", HostUrl = new Uri("https://us.i.posthog.com"), PersonalApiKey = Environment.GetEnvironmentVariable( "PostHog__PersonalApiKey") @@ -228,9 +238,535 @@ PostHog AI } ``` -### Community questions +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +## Capturing events + +You can send custom events using `capture`: + +C# + +PostHog AI + +```csharp +posthog.Capture("distinct_id_of_the_user", "user_signed_up"); +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_the_user", + "user_signed_up", + properties: new() { + ["login_type"] = "email", + ["is_free_trial"] = "true" + } +); +``` + +### Sending page views + +If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send `$pageview` events from your backend like so: + +C# + +PostHog AI + +```csharp +using PostHog; +using Microsoft.AspNetCore.Http.Extensions; +posthog.CapturePageView( + "distinct_id_of_the_user", + HttpContext.Request.GetDisplayUrl()); +``` + +## Request context + +For ASP.NET Core apps using `PostHog.AspNetCore`, add request context middleware before routes that call PostHog. This reads incoming PostHog tracing headers and attaches request metadata to captures, exceptions, and feature flag evaluation inside the request. + +Program.cs + +PostHog AI + +```csharp +using PostHog; +using PostHog.AspNetCore; +var builder = WebApplication.CreateBuilder(args); +builder.AddPostHog(); +var app = builder.Build(); +app.UsePostHogRequestContext(); +``` + +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your ASP.NET Core backend hostname so browser requests include the session and distinct ID headers. + +The middleware reads `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` as request-scoped analytics context. It also adds request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip`. Explicit distinct IDs and event properties always override request context. + +Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side decisions, pass an authenticated distinct ID explicitly. You can ignore tracing headers while still collecting request metadata: + +C# + +PostHog AI + +```csharp +app.UsePostHogRequestContext(options => +{ + options.UseTracingHeaders = false; +}); +``` + +Request-context overloads like `posthog.Capture("checkout started")` and `posthog.EvaluateFlagsAsync()` use the current request distinct ID when one is available. + +## Error tracking + +You can manually capture exceptions using `CaptureException`. This sends a `$exception` event with stack frames, inner exceptions, aggregate exceptions, source context when available, and .NET runtime metadata. + +File names, line numbers, and source context depend on debug information already available from the captured .NET stack trace. PostHog doesn't support uploading .NET PDB files yet, so production builds without runtime-accessible debug information may show less detailed stack frames. + +C# + +PostHog AI + +```csharp +try +{ + ProcessOrder(orderId); +} +catch (Exception exception) +{ + posthog.CaptureException(exception, "user_distinct_id"); +} +``` + +Add custom properties to include request, tenant, or domain context: + +C# + +PostHog AI + +```csharp +posthog.CaptureException( + exception, + "user_distinct_id", + new Dictionary + { + ["order_id"] = orderId, + ["environment"] = "production", + } +); +``` + +For the full setup guide, see the [.NET error tracking installation docs](/docs/error-tracking/installation/dotnet.md). + +Automatic exception capture is not available in the .NET SDK yet. + +## Logs + +[PostHog Logs](/docs/logs.md) doesn't use this SDK. Logs are ingested over OpenTelemetry, so you attach an OTLP exporter to the standard `ILogger` pipeline instead — see the [.NET logs installation guide](/docs/logs/installation/dotnet.md). + +## Person profiles and properties + +The .NET SDK captures identified events by default. These create [person profiles](/docs/data/persons.md). To set [person properties](/docs/data/user-properties.md) in these profiles, include them when capturing an event: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + personPropertiesToSet: new() { ["name"] = "Max Hedgehog" }, + personPropertiesToSetOnce: new() { ["initial_url"] = "/blog" } +); +``` + +For more details on the difference between `$set` and `$set_once`, see our [person properties docs](/docs/data/user-properties.md#what-is-the-difference-between-set-and-set_once). + +To capture [anonymous events](/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's `$process_person_profile` property to `false`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id", + "event_name", + properties: new() { + ["$process_person_profile"] = false + } +) +``` + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +C# + +PostHog AI + +```csharp +await posthog.AliasAsync("current_distinct_id", "new_distinct_id"); +``` + +We strongly recommend reading our docs on [alias](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Group analytics + +Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the [group analytics](/docs/product-analytics/group-analytics.md) guide for more information. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on our [pricing page](/pricing.md). + +To capture an event and associate it with a group, add the `groups` argument to your `Capture` call: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "user_distinct_id", + "some_event", + groups: [new Group("company", "company_id_in_your_db")]); +``` + +Update properties on a group, use the `GroupIdentifyAsync` method: + +C# + +PostHog AI + +```csharp +await posthog.GroupIdentifyAsync( + type: "company", + key: "company_id_in_your_db", + name: "Awesome Inc.", + properties: new() + { + ["employees"] = 11 + } +); +``` + +The `name` is a special property which is used in the PostHog UI for the name of the group. If you don't specify a `name` property, the group ID will be used instead. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Evaluation contexts + +Configure evaluation contexts so this SDK only evaluates flags intended for the matching application, platform, or product area. For ASP.NET Core apps using `PostHog.AspNetCore`, add them to the `PostHog` configuration section: + +JSON + +PostHog AI + +```json +{ + "PostHog": { + "ProjectToken": "", + "HostUrl": "https://us.i.posthog.com", + "EvaluationContexts": ["main-app", "api", "backend"] + } +} +``` + +For code-based configuration, set `EvaluationContexts` on `PostHogOptions`: + +C# + +PostHog AI + +```csharp +var posthog = new PostHogClient(new PostHogOptions +{ + ProjectToken = "", + HostUrl = new Uri("https://us.i.posthog.com"), + EvaluationContexts = ["main-app", "api", "backend"], +}); +``` + +Remote `/flags` requests from `EvaluateFlagsAsync()` include `evaluation_contexts` when configured. + +For more details, see the [evaluation contexts guide](/docs/feature-flags/evaluation-contexts.md). + +### Local evaluation + +Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. + +It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls. + +For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation.md). + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("user_distinct_id"); +var variant = flags.GetFlag("experiment-feature-flag-key")?.VariantKey; +if (variant == "variant-name") +{ + // Do something +} +``` + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## AI observability + +`PostHog.AI` adds [AI observability](/docs/ai-observability.md) for .NET applications using OpenAI or Azure OpenAI. It is currently pre-release, so expect breaking changes before a stable release. + +For installation instructions, see the [OpenAI guide for .NET](/docs/ai-observability/installation/openai.md#net-support) or the [Azure OpenAI guide for .NET](/docs/ai-observability/installation/azure-openai.md#net-support). + +## GeoIP properties + +The `posthog-dotnet` library disregards the server IP, does not add the GeoIP properties, and does not use the values for feature flag evaluations. + +## Serverless environments (Azure Functions/Render/Lambda/...) + +By default, the library buffers events before sending them to the `/batch` endpoint for better performance. This can lead to lost events in serverless environments if the .NET process is terminated by the platform before the buffer is fully flushed. + +To avoid this, call `await posthog.FlushAsync()` after processing every request by adding it as a middleware to your server. This allows `posthog.Capture()` to remain asynchronous for better performance. + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-elixir/SKILL.md b/skills/posthog/all/skills/feature-flags-elixir/SKILL.md index 45e38c64..ce303e2f 100644 --- a/skills/posthog/all/skills/feature-flags-elixir/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-elixir/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-elixir description: PostHog feature flags for Elixir applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Elixir @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Elixir applications. - `references/elixir.md` - Elixir feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,15 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get` +- Configure PostHog in application config using `api_host`, `api_key`, and `in_app_otp_apps`; read secrets from environment or runtime config, never hardcode them +- In tests, set `test_mode` to true so events are dropped instead of sent to PostHog +- For Phoenix or Plug apps, add `PostHog.Integrations.Plug` before the router so request context is attached to captured events and errors +- Server-side captures must include a stable `distinct_id` matching frontend identify calls, or set it once per process/request with `PostHog.set_context/1` +- Remember `PostHog.set_context/1` uses Logger metadata and is process-scoped; set context in the request, job, or Task process that captures the event +- For new feature flag code, prefer `PostHog.FeatureFlags.evaluate_flags/1` once per user/request, then read values from `PostHog.FeatureFlags.Evaluations` +- To attribute captures to feature flags, call `PostHog.FeatureFlags.set_in_context/1` with the evaluated snapshot, optionally filtered with `only_accessed/1` or `only/2` +- Avoid deprecated feature flag helpers such as `check/2`, `check!/2`, `get_feature_flag_result/2`, and `get_feature_flag_result!/2` in new code +- Error tracking is enabled by default through Logger; set `in_app_otp_apps`, `capture_level`, and `metadata` to improve error grouping and context +- For source context in releases, enable source code context and run `mix posthog.package_source_code` before `mix release` diff --git a/skills/posthog/all/skills/feature-flags-elixir/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-elixir/references/COMMANDMENTS.md new file mode 100644 index 00000000..69522fb4 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-elixir/references/COMMANDMENTS.md @@ -0,0 +1,16 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-elixir is installed as the `posthog` Hex package; add `{:posthog, "~> 2.0"}` to `mix.exs` and run `mix deps.get` +- Configure PostHog in application config using `api_host`, `api_key`, and `in_app_otp_apps`; read secrets from environment or runtime config, never hardcode them +- In tests, set `test_mode` to true so events are dropped instead of sent to PostHog +- For Phoenix or Plug apps, add `PostHog.Integrations.Plug` before the router so request context is attached to captured events and errors +- Server-side captures must include a stable `distinct_id` matching frontend identify calls, or set it once per process/request with `PostHog.set_context/1` +- Remember `PostHog.set_context/1` uses Logger metadata and is process-scoped; set context in the request, job, or Task process that captures the event +- For new feature flag code, prefer `PostHog.FeatureFlags.evaluate_flags/1` once per user/request, then read values from `PostHog.FeatureFlags.Evaluations` +- To attribute captures to feature flags, call `PostHog.FeatureFlags.set_in_context/1` with the evaluated snapshot, optionally filtered with `only_accessed/1` or `only/2` +- Avoid deprecated feature flag helpers such as `check/2`, `check!/2`, `get_feature_flag_result/2`, and `get_feature_flag_result!/2` in new code +- Error tracking is enabled by default through Logger; set `in_app_otp_apps`, `capture_level`, and `metadata` to improve error grouping and context +- For source context in releases, enable source code context and run `mix posthog.package_source_code` before `mix release` diff --git a/skills/posthog/all/skills/feature-flags-elixir/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-elixir/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-elixir/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-elixir/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-elixir/references/best-practices.md b/skills/posthog/all/skills/feature-flags-elixir/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-elixir/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-elixir/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-elixir/references/elixir.md b/skills/posthog/all/skills/feature-flags-elixir/references/elixir.md index ac75276d..204e5550 100644 --- a/skills/posthog/all/skills/feature-flags-elixir/references/elixir.md +++ b/skills/posthog/all/skills/feature-flags-elixir/references/elixir.md @@ -1,4 +1,10 @@ -# Elixir feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Elixir Feature Flags installation - Docs + +Copy page + +# Elixir Feature Flags installation - Docs > This library was built by the community but it's being maintained by the PostHog core team since v1.0.0. Thank you to [Nick Kezhaya](https://github.com/nkezhaya) for building it originally. Thank you to [Alex Martsinovich](https://github.com/martosaur) for contributing v2.0.0. @@ -11,7 +17,7 @@ PostHog AI ```elixir def deps do [ - {:posthog, "~> 2.2.0"} + {:posthog, "~> 2.0"} ] end ``` @@ -32,15 +38,15 @@ config :posthog, You can see all the available configuration options in the [PostHog.Config](https://hexdocs.pm/posthog/PostHog.Config.html) module. -Optionally, you might want to enable the [Plug integration](https://hexdocs.pm/posthog/PostHog.Integrations.Plug.html) to automatically capture events from your Plug-based applications including Phoenix. +Optionally, you might want to enable the [Plug integration](https://hexdocs.pm/posthog/PostHog.Integrations.Plug.html) to attach request metadata and tracing context in Plug-based applications including Phoenix. You still need to capture events explicitly with `PostHog.capture/2` or `PostHog.capture/3`. #### Development/Test mode For a test environment, you can pass in `test_mode: true` value to the config. This causes events to be dropped instead of sent to PostHog. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-flask/SKILL.md b/skills/posthog/all/skills/feature-flags-flask/SKILL.md new file mode 100644 index 00000000..42bbc5de --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/SKILL.md @@ -0,0 +1,49 @@ +--- +name: feature-flags-flask +description: PostHog feature flags for Flask applications +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for Flask + +This skill helps you add PostHog feature flags to Flask applications. + +## Reference files + +- `references/python.md` - Python feature flags installation - docs +- `references/flask.md` - Flask - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) +- Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers +- Blueprint registration happens AFTER PostHog initialization in create_app() +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/feature-flags-flask/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-flask/references/COMMANDMENTS.md new file mode 100644 index 00000000..0beac1a3 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/references/COMMANDMENTS.md @@ -0,0 +1,18 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) +- Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers +- Blueprint registration happens AFTER PostHog initialization in create_app() +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/feature-flags-flask/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-flask/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-flask/references/best-practices.md b/skills/posthog/all/skills/feature-flags-flask/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-flask/references/flask.md b/skills/posthog/all/skills/feature-flags-flask/references/flask.md new file mode 100644 index 00000000..560fa82f --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/references/flask.md @@ -0,0 +1,147 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Flask - Docs + +Copy page + +# Flask - Docs + +PostHog makes it easy to get data about traffic and usage of your Flask app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more. + +This guide walks you through integrating PostHog into your Flask app using the [Python SDK](/docs/libraries/python.md). + +> These docs cover version `7.x` of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See [supported versions](#supported-versions). + +## Installation + +To start, run `pip install posthog` to install PostHog’s Python SDK. + +Then, initialize PostHog where you'd like to use it. For example, here's how to capture an event in a simple route: + +app.py + +PostHog AI + +```python +from flask import Flask +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog( + '', + host='https://us.i.posthog.com', +) +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + posthog.capture( + 'dashboard_api_called', + distinct_id='distinct_id_of_your_user', + ) + return '', 204 +``` + +You can find your project token and instance address in [your project settings](https://app.posthog.com/project/settings). + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` to associate events with the correct user. +> +> In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct `distinct_id`. Typically, you would set a fresh context and identify at the top of each route. +> +> Python +> +> PostHog AI +> +> ```python +> from posthog import new_context, identify_context, capture +> @app.get("/foo") +> def foo(current_user: User = Depends(get_current_user)): +> with new_context(): # Set context at the top of a route +> identify_context(current_user.id) +> capture("foo_viewed") +> return {"status": "ok"} +> ``` +> +> When possible, write a small piece of **middleware** that resolves your authenticated user, wrap a context around the request, and identifies it. Every `capture()` downstream is then attributed *automatically*. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK. + +## Request contexts + +Use [contexts](/docs/libraries/python.md#contexts) to share identity, session IDs, and tags across multiple captures during a request. + +If you're using [PostHog JavaScript Web](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Flask backend hostname so browser requests include the session and distinct ID headers. + +Then read the incoming headers in your Flask request handler. Tracing headers are client-controlled analytics context, not authentication or authorization, so prefer your authenticated user ID when one is available: + +Python + +PostHog AI + +```python +from flask import request, session +from posthog import identify_context, set_context_session, tag +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + with posthog.new_context(fresh=True): + distinct_id = session.get('user_id') or request.headers.get('X-POSTHOG-DISTINCT-ID') + if distinct_id: + identify_context(str(distinct_id)) + session_id = request.headers.get('X-POSTHOG-SESSION-ID') + if session_id: + set_context_session(session_id) + tag('$current_url', request.url) + tag('$request_method', request.method) + tag('$request_path', request.path) + posthog.capture('dashboard_api_called') + return '', 204 +``` + +Events captured without a context or explicit `distinct_id` are sent as [anonymous events](/docs/data/anonymous-vs-identified-events.md) with an auto-generated `distinct_id`. See the [Python SDK docs](/docs/libraries/python.md#person-profiles-and-properties) for more details. + +## Error tracking + +Flask has built-in error handlers. This means PostHog’s default exception autocapture won’t work and we need to manually capture errors instead using `capture_exception()`: + +Python + +PostHog AI + +```python +from flask import Flask, jsonify +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog('', host='https://us.i.posthog.com') +@app.errorhandler(Exception) +def handle_exception(e): + # Capture methods, including capture_exception, return the UUID of the captured event, + # which you can use to find specific errors users encountered + event_id = posthog.capture_exception(e) + # You can show the event ID to your user, and ask them to include it in bug reports + response = jsonify({'message': str(e), 'error_id': event_id}) + response.status_code = 500 + return response +``` + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Flask (such as analytics, feature flags, A/B testing, etc.), have a look at our [Python SDK docs](/docs/libraries/python.md). + +Alternatively, the following tutorials can help you get started: + +- [How to set up analytics in Python and Flask](/tutorials/python-analytics.md) +- [How to set up feature flags in Python and Flask](/tutorials/python-feature-flags.md) +- [How to set up A/B tests in Python and Flask](/tutorials/python-ab-testing.md) + +## Supported versions + +These docs cover version `7.x` of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on `7.x.x` and higher — pin to the 6.x line with `pip install 'posthog<7'`, where `6.9.3` is the final release. + +Everything on this page works the same way on `6.9.3`. Event capture, the context API (`new_context`, `identify_context`, `set_context_session`), and `PosthogContextMiddleware` are identical on `6.9.3` and `7.0.0` — `7.0.0` only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the `X-POSTHOG-DISTINCT-ID` header and falling back to the authenticated user, which behaves the same across both lines. + +Later `7.x` releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and `set_context_device_id`. They also changed the middleware's own captured properties: `7.x` sends the request IP as `$ip`, where `6.9.3` sends it as `$ip_address`, and `7.x` additionally captures `$request_path`, `$raw_user_agent`, and the authenticated user's `email`. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-flask/references/python.md b/skills/posthog/all/skills/feature-flags-flask/references/python.md new file mode 100644 index 00000000..30634ac5 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flask/references/python.md @@ -0,0 +1,197 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Feature Flags installation - Docs + +Copy page + +# Python Feature Flags installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog Python library using pip: + + Terminal + + PostHog AI + + ```bash + pip install posthog + ``` + +2. 2 + + ## Initialize PostHog + + Required + + Initialize the PostHog client with your project token and host from your project settings: + + Python + + PostHog AI + + ```python + from posthog import Posthog + posthog = Posthog( + project_api_key='', + host='https://us.i.posthog.com' + ) + ``` + + **Django integration** + + If you're using Django, check out our [Django integration](/docs/libraries/django.md) for automatic request tracking. + +3. 3 + + ## Send events + + Recommended + + Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration: + + Capture custom events by calling the `capture` method with an event name and properties: + + Python + + PostHog AI + + ```python + import posthog + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) + ``` + +4. 4 + + ## Evaluate boolean feature flags + + Required + + Check if a feature flag is enabled: + + ```python + is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') + if is_my_flag_enabled: + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + ``` + +5. 5 + + ## Evaluate multivariate feature flags + + Optional + + For multivariate flags, check which variant the user has been assigned: + + ```python + enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') + if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + ``` + +6. 6 + + ## Include feature flag information in events + + Required + + If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + + **Note:** This step is only required for events captured using our server-side SDKs or API. + + ## Set send_feature_flags (recommended) + + Set `send_feature_flags` to `True` in your capture call: + + Python + + PostHog AI + + ```python + posthog.capture( + distinct_id="distinct_id_of_the_user", + event='event_name', + send_feature_flags=True + ) + ``` + + ## Include $feature property + + Include the `$feature/feature_flag_name` property in your event properties: + + Python + + PostHog AI + + ```python + posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + }, + ) + ``` + +7. 7 + + ## Override server properties + + Optional + + Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with: + + ```python + posthog.get_feature_flag( + 'flag-key', + 'distinct_id_of_the_user', + person_properties={'property_name': 'value'}, + groups={ + 'your_group_type': 'your_group_id', + 'another_group_type': 'your_group_id'}, + group_properties={ + 'your_group_type': {'group_property_name': 'value'}, + 'another_group_type': {'group_property_name': 'value'} + }, + ) + ``` + +8. 8 + + ## Running experiments + + Optional + + Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard. + +9. 9 + + ## Next steps + + Recommended + + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. + + | Resource | Description | + | --- | --- | + | [Creating a feature flag](/docs/feature-flags/creating-feature-flags.md) | How to create a feature flag in PostHog | + | [Adding feature flag code](/docs/feature-flags/adding-feature-flag-code.md) | How to check flags in your code for all platforms | + | [Framework-specific guides](/docs/feature-flags/tutorials.md#framework-guides) | Setup guides for React Native, Next.js, Flutter, and other frameworks | + | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | + | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-flutter/SKILL.md b/skills/posthog/all/skills/feature-flags-flutter/SKILL.md index df89564a..415d09ee 100644 --- a/skills/posthog/all/skills/feature-flags-flutter/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-flutter/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-flutter description: PostHog feature flags for Flutter applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Flutter @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Flutter applications. - `references/flutter.md` - Flutter feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,13 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/feature-flags-flutter/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-flutter/references/COMMANDMENTS.md new file mode 100644 index 00000000..58624ee9 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-flutter/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog_flutter is the Flutter SDK package name; install it with `flutter pub add posthog_flutter` or add it to `pubspec.yaml` +- For manual setup, call `WidgetsFlutterBinding.ensureInitialized()`, create a `PostHogConfig`, then await `Posthog().setup(config)` before `runApp()` +- For Android, ensure `minSdkVersion` is at least `23`. If the current value is lower than `23` or missing, update/add it as `minSdkVersion 23`; if it is already `23` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `android/app/src/main/AndroidManifest.xml` unless using manual setup with `AUTO_INIT=false` +- For iOS, ensure the minimum deployment target is at least iOS `13.0`. If the current `platform :ios` value is lower than `13.0` or missing, update/add it as `platform :ios, '13.0'`; if it is already `13.0` or higher, leave it unchanged. Configure `com.posthog.posthog.PROJECT_TOKEN` and `com.posthog.posthog.POSTHOG_HOST` in `ios/Runner/Info.plist` unless using manual setup +- For Session Replay or Surveys, disable auto-init with `com.posthog.posthog.AUTO_INIT=false` and initialize manually so the required options can be enabled +- For Flutter Web, add the posthog-js web snippet to `web/index.html`. If you are instructed to ever embed the HTML snippet into the user's code, write the real token directly into the snippet. It is not a secret, and when used as an HTML snippet, it should be written in literally. The example token phc_your_project_token_here is a placeholder for readers. It is not the shape to copy. Flutter Web session replay also requires Canvas capture in project settings +- Capture screen views by adding `PosthogObserver()` to the app's `navigatorObservers`, whatever routing package the app uses; where its routes are unnamed, name them so `$screen` is readable +- Call `Posthog().identify(...)` after login and `Posthog().reset()` on logout; keep PII in user properties, not event properties +- Use `beforeSend` to redact or drop Dart-captured events, but remember it does not intercept native session replay, lifecycle, or system properties diff --git a/skills/posthog/all/skills/feature-flags-flutter/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-flutter/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-flutter/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-flutter/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-flutter/references/best-practices.md b/skills/posthog/all/skills/feature-flags-flutter/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-flutter/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-flutter/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-flutter/references/flutter.md b/skills/posthog/all/skills/feature-flags-flutter/references/flutter.md index 46075dfe..e9db83b5 100644 --- a/skills/posthog/all/skills/feature-flags-flutter/references/flutter.md +++ b/skills/posthog/all/skills/feature-flags-flutter/references/flutter.md @@ -1,4 +1,10 @@ -# Flutter feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Flutter Feature Flags installation - Docs + +Copy page + +# Flutter Feature Flags installation - Docs 1. 1 @@ -13,7 +19,7 @@ PostHog AI ```yaml - posthog_flutter: ^5.0.0 + posthog_flutter: ^5.24.0 ``` 2. 2 @@ -35,7 +41,7 @@ [...] - + @@ -50,7 +56,7 @@ ```groovy defaultConfig { - minSdkVersion 21 + minSdkVersion 23 // rest of your config } ``` @@ -66,7 +72,7 @@ ```xml [...] - com.posthog.posthog.API_KEY + com.posthog.posthog.PROJECT_TOKEN com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com @@ -102,10 +108,10 @@ ... @@ -154,7 +160,7 @@ if (isMyFlagEnabled) { // Do something differently for this user // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + final matchedFlagPayload = (await Posthog().getFeatureFlagResult('flag-key'))?.payload; } ``` @@ -175,7 +181,7 @@ if (enabledVariant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + final matchedFlagPayload = (await Posthog().getFeatureFlagResult('flag-key'))?.payload; } ``` @@ -203,9 +209,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-go/SKILL.md b/skills/posthog/all/skills/feature-flags-go/SKILL.md index 204266da..c7f5607d 100644 --- a/skills/posthog/all/skills/feature-flags-go/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-go/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-go description: PostHog feature flags for Go applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Go @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Go applications. - `references/go.md` - Go feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,14 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/feature-flags-go/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-go/references/COMMANDMENTS.md new file mode 100644 index 00000000..68b721fe --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-go/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-go is the Go SDK package; install it with `go get github.com/posthog/posthog-go` and import `github.com/posthog/posthog-go` +- Create one PostHog client per process with `posthog.NewWithConfig(...)`; do not create a new client per request or job +- Always close the client during graceful shutdown with `client.Close()` so queued events flush before the process exits +- Configure the project token, endpoint, and optional personal API key from environment variables; never hardcode PostHog secrets +- Server-side captures must set `DistinctId` to a stable user ID that matches frontend identify calls; avoid anonymous or literal IDs for business events +- Use `posthog.NewProperties().Set(...)` for event properties and keep PII in person properties via `$set`, not in event properties +- For new feature flag code, prefer `client.EvaluateFlags(...)` once per user/request, then use the returned snapshot's `IsEnabled` or `GetFlag` methods +- When capturing events related to feature-gated code, attach the evaluated flag snapshot with `Flags`, optionally filtered with `OnlyAccessed()` or `Only(...)` +- Avoid deprecated feature flag helpers such as `IsFeatureEnabled`, `GetFeatureFlag`, `GetFeatureFlagPayload`, and `Capture.SendFeatureFlags` in new code +- For error tracking, use `posthog.NewDefaultException(...)` for direct captures or wrap `log/slog` with `posthog.NewSlogCaptureHandler(...)` for automatic warning-and-above exception capture diff --git a/skills/posthog/all/skills/feature-flags-go/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-go/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-go/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-go/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-go/references/best-practices.md b/skills/posthog/all/skills/feature-flags-go/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-go/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-go/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-go/references/go.md b/skills/posthog/all/skills/feature-flags-go/references/go.md index c567a036..194206d6 100644 --- a/skills/posthog/all/skills/feature-flags-go/references/go.md +++ b/skills/posthog/all/skills/feature-flags-go/references/go.md @@ -1,4 +1,10 @@ -# Go feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Go Feature Flags installation - Docs + +Copy page + +# Go Feature Flags installation - Docs 1. 1 @@ -199,9 +205,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ios/SKILL.md b/skills/posthog/all/skills/feature-flags-ios/SKILL.md index 751278f3..3721330c 100644 --- a/skills/posthog/all/skills/feature-flags-ios/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-ios/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-ios description: PostHog feature flags for iOS applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for iOS @@ -13,8 +13,10 @@ This skill helps you add PostHog feature flags to iOS applications. ## Reference files - `references/ios.md` - Ios feature flags installation - docs +- `references/usage.md` - Ios SDK usage - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,7 +33,19 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -- Read configuration from environment variables via a `PostHogEnv` enum with a `value` computed property that calls `ProcessInfo.processInfo.environment[rawValue]` and `fatalError`s if missing — cases should be `projectToken = "POSTHOG_PROJECT_TOKEN"` and `host = "POSTHOG_HOST"`, set in the Xcode scheme's Run environment variables -- When adding SPM dependencies to project.pbxproj, create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. - Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version - If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/feature-flags-ios/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-ios/references/COMMANDMENTS.md new file mode 100644 index 00000000..dd2c05a5 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ios/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Install the PostHog iOS SDK as `PostHog` via Swift Package Manager or CocoaPods, using `https://github.com/PostHog/posthog-ios.git` for SPM +- Initialize `PostHogSDK.shared.setup(config)` exactly once and as early as possible, either in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` or in the SwiftUI `App` initializer +- For SwiftUI apps, prefer meaningful `.postHogScreenView(...)` modifiers for screen tracking because automatic SwiftUI screen names can be internal view identifiers +- Call `PostHogSDK.shared.identify(...)` after login and `PostHogSDK.shared.reset()` on logout; keep PII in user properties, not event properties +- Enable iOS error autocapture with `config.errorTrackingConfig.autoCapture = true` and upload dSYM files so crash reports are symbolicated +- Enable session replay with `config.sessionReplay = true` only after confirming project replay settings and privacy masking requirements; session replay is iOS-only, not macOS +- Use `config.setBeforeSend { event in ... }` to redact, drop, or sample custom events, while preserving PostHog internal events where possible +- For iOS logs, use posthog-ios 3.58.0 or later, set `config.logs` fields before `setup`, and capture logs manually with `PostHogSDK.shared.logger` or `captureLog` +- For widgets, app clips, share extensions, and other app extensions, configure `config.appGroupIdentifier` so the main app and extensions share analytics identity +- Set the PostHog project token and host directly in code when creating the `PostHogConfig` (e.g. `PostHogConfig(apiKey: "", host: "https://us.i.posthog.com")`). The project token is a public client-side key designed to ship in the app binary, so hardcoding it is safe and is the recommended approach for iOS +- Do NOT depend on Xcode scheme environment variables (`ProcessInfo.processInfo.environment`) as the only source of the token: they are injected only when launching from Xcode (debug/simulator), NOT in Archive/Release builds (TestFlight, App Store). Reading them is fine as an optional override, but never force-unwrap or `fatalError` on their absence — that crashes production builds on launch. Ensure a value always ships in the binary +- Before editing any Xcode project file, check for a project generator spec. If a `project.yml` with XcodeGen-shaped content (top-level `targets:` and/or `packages:` keys — do not trust the filename alone) exists at the repo root, the `.xcodeproj` is generated and MUST NOT be edited directly: the next `xcodegen generate` silently wipes any edit to `project.pbxproj`. Instead declare the package in `project.yml` under `packages:` as `PostHog: { url: https://github.com/PostHog/posthog-ios, from: }`, add `- package: PostHog` to the app target's `dependencies:` list, then tell the user to re-run `xcodegen generate` to apply it +- When adding SPM dependencies to project.pbxproj (only when no XcodeGen `project.yml` generator spec exists — see the rule above), create three distinct objects with unique UUIDs — a `PBXBuildFile` (with `productRef`), an `XCSwiftPackageProductDependency` (with `package` and `productName`), and an `XCRemoteSwiftPackageReference` (with `repositoryURL` and `requirement`). The build file goes in the Frameworks phase `files`, the product dependency goes in the target's `packageProductDependencies`, and the package reference goes in the project's `packageReferences`. +- Check the latest release version of posthog-ios at `https://github.com/PostHog/posthog-ios/releases` before setting the `minimumVersion` in the SPM package reference — do not hardcode a stale version +- If the project uses App Sandbox (macOS), add `ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES` to the target's build settings so PostHog can reach its servers — do NOT disable the sandbox entirely diff --git a/skills/posthog/all/skills/feature-flags-ios/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-ios/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-ios/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-ios/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ios/references/best-practices.md b/skills/posthog/all/skills/feature-flags-ios/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-ios/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-ios/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ios/references/ios.md b/skills/posthog/all/skills/feature-flags-ios/references/ios.md index 2d717604..82ead6f3 100644 --- a/skills/posthog/all/skills/feature-flags-ios/references/ios.md +++ b/skills/posthog/all/skills/feature-flags-ios/references/ios.md @@ -1,4 +1,10 @@ -# iOS feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS Feature Flags installation - Docs + +Copy page + +# iOS Feature Flags installation - Docs 1. 1 @@ -14,7 +20,7 @@ ```swift dependencies: [ - .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.0.0") + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.56.0") ] ``` @@ -25,7 +31,7 @@ PostHog AI ```ruby - pod "PostHog", "~> 3.0" + pod "PostHog", "~> 3.56" ``` 2. 2 @@ -48,7 +54,7 @@ func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { let POSTHOG_PROJECT_TOKEN = "" let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -88,7 +94,7 @@ if isMyFlagEnabled { // Do something differently for this user // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") + let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagResult("flag-key")?.payload } ``` @@ -109,7 +115,7 @@ if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") + let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagResult("flag-key")?.payload } ``` @@ -137,9 +143,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ios/references/usage.md b/skills/posthog/all/skills/feature-flags-ios/references/usage.md new file mode 100644 index 00000000..906f6b5c --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ios/references/usage.md @@ -0,0 +1,641 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS SDK usage - Docs + +Copy page + +# iOS SDK usage - Docs + +## Capturing events + +You can send custom events using `capture`: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("user_signed_up") +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("user_signed_up", properties: ["login_type": "email"], userProperties: ["is_free_trial": true]) +``` + +## Autocapture + +PostHog autocapture automatically tracks the following events for you: + +- **Application Opened** – when the app is opened from a closed state or when the app comes to the foreground (e.g. from the app switcher) +- **Application Backgrounded** – when the app is sent to the background by the user +- **Application Installed** – when the app is installed +- **Application Updated** – when the app is updated +- **$screen** – when the user navigates (if using `UIViewController`) +- **$autocapture** – when the user interacts with elements in a screen (`UIKit based`) and `captureElementInteractions` is enabled +- **$rageclick** – when the user rapidly taps in the same area (iOS/macCatalyst, `UIKit based`) + +> 🚧 **Note:** `$autocapture` and `$rageclick` are captured from UIKit interactions. Some SwiftUI views use UIKit under the hood (for example, `TextField` → `UITextField` and `Toggle` → `UISwitch`), so those interactions may also be autocaptured. In other SwiftUI cases, interactions might still be captured, but element metadata (such as `$elements_chain`) may be incomplete. + +### Capturing screen views + +With [`configuration.captureScreenViews`](/docs/libraries/ios/configuration.md#all-configuration-options) set as `true`, PostHog will try to record all screen changes automatically. + +If you want to manually send a new screen capture event, use the `screen` function. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.screen("Dashboard", properties: ["fromIcon": "bottom"]) +``` + +> **Important:** While `captureScreenViews` works with both `UIKit` and `SwiftUI`, the screen names captured in `SwiftUI` may not be very meaningful as they are based on internal SwiftUI view identifiers. For `SwiftUI` applications, we recommend turning this option off and instead using the `.postHogScreenView()` view modifier (see next section) to capture screen views with meaningful names. + +> **Note:** You can use the `BeforeSendBlock` to filter or drop any undesired screen events, giving you control over which screen views are sent to PostHog. See [Amending, dropping or sampling events](/docs/libraries/ios.md#amending-dropping-or-sampling-events) for implementation examples. + +### Capturing screen views in SwiftUI + +To track a screen view in `SwiftUI`, apply the `postHogScreenView` modifier to your full-screen views. PostHog will send a `$screen` event when the `onAppear` action is executed and will infer a screen name based on the view's type. You can provide a custom name and event properties if needed. + +HomeView.swift + +PostHog AI + +```swift +// This will trigger a screen view event with $screen_name: "HomeViewContent" +struct HomeView: View { + var body: some View { + HomeViewContent() + .postHogScreenView() + } +} +// This will trigger a screen view event with $screen_name: "My Home View" and an additional event property from_button: "start" +struct HomeView: View { + var body: some View { + HomeViewContent() + .postHogScreenView("My Home View", ["from_button": "start"]) + } +} +``` + +In SwiftUI, views can range from entire screens to small UI components. Unlike UIKit, SwiftUI doesn't clearly distinguish between these levels, which makes automatic tracking of full-screen views harder. + +### Adding a custom label on autocaptured elements + +PostHog automatically captures interactions with various UI elements in your app, but these interactions are often identified by element type names (e.g., UIButton, UITextField, UILabel). + +While this provides basic tracking, it can be challenging to pinpoint specific interactions with particular elements in your analytics. To make your data more meaningful and actionable, you can assign custom labels to any autocaptured element. These labels act as descriptive identifiers, making it easier to identify, filter, and analyze events in your reports. + +**Adding a custom label in UIKit** + +To assign a custom label to a UIView, use the `postHogLabel` property: + +Swift + +PostHog AI + +```swift +let view = UIView() +view.postHogLabel = "usernameTextField" +``` + +In this example, interactions with the UITextField will be captured with an additional identifier "usernameTextField". + +**Adding a custom label in SwiftUI** + +In SwiftUI, use the `.postHogLabel(_:)` modifier instead: + +Swift + +PostHog AI + +```swift +var body: some View { + ... + TextField("username", text: $username) + .postHogLabel("usernameTextField") +} +``` + +Since SwiftUI's `TextField` uses `UITextField` under the hood, interactions with it will be autocaptured with the additional identifier "usernameTextField". + +**Example of generated analytics data** + +The generated analytics element in the examples above will have the following form: + +Swift + +PostHog AI + +```swift +text value +``` + +**Filtering for labeled autocaptured elements in reports** + +To locate and filter interactions with specific elements in PostHog reports, you can use Autocapture element filters, such as: + +- Tag Name (`UITextField` in this example) +- Text (`text value` in this example) +- CSS Selector (the generated `id` attribute in this example) + +In the examples above, we can filter for the specific text field using the CSS Selector `#usernameTextField` + +### Interaction autocapture + +Interaction autocapture records when users interact with UI elements in your app. This includes: + +- User interactions like `touch`, `swipe`, `pan`, `pinch`, `rotation`, `long_press`, `scroll` +- Control types `value_changed`, `submit`, `toggle`, `primary_action`, `menu_action`, `change` + +Interaction autocapture is **not enabled by default**. You can enable it by setting `captureElementInteractions` to `true` in the config. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.captureElementInteractions = true // Disabled by default +PostHogSDK.shared.setup(config) +``` + +### Rage click autocapture + +> **Note:** Rage click autocapture for iOS/macCatalyst is available in version 3.51.0+. + +A rage click is when a user taps an area multiple times in quick succession (e.g more than 3 taps in 1 second). + +This is captured as a `$rageclick` event. You can use this event to identify opportunities to improve your UI, since it's a good indication that users may be frustrated with your product. + +It is enabled by default (`rageClickConfig.enabled = true`). + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.rageClickConfig.enabled = true // Enabled by default +config.rageClickConfig.minimumTapCount = 3 // Optional, default is 3 +config.rageClickConfig.thresholdPoints = 30 // Optional, default is 30 +config.rageClickConfig.timeoutInterval = 1.0 // Optional, default is 1.0s +PostHogSDK.shared.setup(config) +``` + +### Autocapture configuration + +You can enable or disable autocapture through the `PostHogConfig` object. Find more details about autocapture configuration in the [configuration page](/docs/libraries/ios/configuration.md#autocapture-configuration). + +## Preventing sensitive data capture + +To exclude specific UI elements from autocapture or Session Replay, add `ph-no-capture` as either an `accessibilityLabel` or `accessibilityIdentifier`. See [privacy controls](/docs/session-replay/privacy?tab=iOS.md) for masking behavior and iOS examples. + +## Identifying users + +> We highly recommend reading our section on [Identifying users](/docs/integrate/identifying-users.md) to better understand how to correctly use this method. + +Using `identify`, you can associate events with specific users. This enables you to gain full insights as to how they're using your product across different sessions, devices, and platforms. + +An `identify` call has the following arguments: + +- `distinct_id` which uniquely identifies your user in your database + +- **userProperties:** Optional. A dictionary with key:value pairs to set the [person properties](/docs/product-analytics/person-properties.md) +- **userPropertiesSetOnce:** Optional. Similar to `userProperties`. [See the difference between `userProperties` and `userPropertiesSetOnce`](/docs/product-analytics/person-properties.md#what-is-the-difference-between-set-and-set_once) + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.identify("user_id_from_your_database", + userProperties: ["name": "Peter Griffin", "email": "peter@familyguy.com"], + userPropertiesSetOnce: ["date_of_first_log_in": "2024-03-01"]) +``` + +You should call `identify` as soon as you're able to. Typically, this is after your user logs in. This ensures that events sent during your user's sessions are correctly associated with them. + +When you call `identify`, all previously tracked anonymous events will be linked to the user. + +## Get the current user's distinct ID + +You may find it helpful to get the current user's distinct ID. For example, to check whether you've already called `identify` for a user or not. + +To do this, call `getDistinctId()`. This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to `identify()`. + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.alias("alias_id") +``` + +We strongly recommend reading our docs on [alias](/docs/data/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Anonymous vs identified events + +PostHog captures two types of events: [**anonymous** and **identified**](/docs/data/anonymous-vs-identified-events.md) + +**Identified events** enable you to attribute events to specific users, and attach [person properties](/docs/product-analytics/person-properties.md). They're best suited for logged-in users. + +Scenarios where you want to capture identified events are: + +- Tracking logged-in users in B2B and B2C SaaS apps +- Doing user segmented product analysis +- Growth and marketing teams wanting to analyze the *complete* conversion lifecycle + +**Anonymous events** are events without individually identifiable data. They're best suited for [web analytics](/docs/web-analytics.md) or apps where users aren't logged in. + +Scenarios where you want to capture anonymous events are: + +- Tracking a marketing website +- Content-focused sites +- B2C apps where users don't sign up or log in + +Under the hood, the key difference between identified and anonymous events is that for identified events we create a [person profile](/docs/data/persons.md) for the user, whereas for anonymous events we do not. + +> **Important:** Due to the reduced cost of processing them, anonymous events can be up to 4x cheaper than identified ones, so we recommended you only capture identified events when needed. + +### How to capture anonymous events + +The iOS SDK captures anonymous events by default. However, this may change depending on your `personProfiles` [config](/docs/libraries/ios/configuration.md#all-configuration-options) when initializing PostHog: + +1. `personProfiles: .identifiedOnly` *(recommended)* *(default)* - Anonymous events are captured by default. PostHog only captures identified events for users where [person profiles](/docs/data/persons.md) have already been created. + +2. `personProfiles: .always` - Capture identified events for all events. + +3. `personProfiles: .never` - Capture anonymous events for all events. + +For example: + +iOS + +PostHog AI + +```swift +let config = PostHogConfig( + projectToken: POSTHOG_PROJECT_TOKEN, + host: POSTHOG_HOST +) +config.personProfiles = .identifiedOnly +PostHogSDK.shared.setup(config) +``` + +### How to capture identified events + +If you've set the [`personProfiles` config](/docs/libraries/ios/configuration.md#all-configuration-options) to `.identifiedOnly` (the default option), anonymous events are captured by default. Then, to capture identified events, call any of the following functions: + +- [`identify()`](/docs/product-analytics/identify.md) +- [`alias()`](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) +- [`group()`](/docs/product-analytics/group-analytics.md) + +When you call any of these functions, it creates a [person profile](/docs/data/persons.md) for the user. Once this profile is created, all subsequent events for this user will be captured as identified events. + +Alternatively, you can set `personProfiles` to `.always` to capture identified events by default. + +## Setting person properties + +To set [properties](/docs/data/user-properties.md) on your users via an event, you can leverage the event properties `userProperties` and `userPropertiesSetOnce`. + +When capturing an event, you can pass a property called `$set` as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userProperties: ["user_property_name": "your_value"]) +``` + +`userPropertiesSetOnce` works just like `userProperties`, except that it will **only set the property if the user doesn't already have that property set**. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userPropertiesSetOnce: ["user_property_name": "your_value"]) +``` + +Use `setPersonProperties` when you want to update the current person's profile without also capturing a custom event. This sends a `$set` event to PostHog. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.setPersonProperties(userPropertiesToSet: ["plan": "Pro++"]) +PostHogSDK.shared.setPersonProperties( + userPropertiesToSet: ["plan": "Pro++"], + userPropertiesToSetOnce: ["first_seen_source": "ios"] +) +``` + +## Super properties + +Super properties are properties associated with events that are set once and then sent with every `capture` call, be it a `$screen`, or anything else. + +They are set using `PostHogSDK.shared.register`, which takes a properties object as a parameter, and they persist across sessions. + +For example, take a look at the following call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.register(["team_id": 22]) +``` + +The call above ensures that every event sent by the user will include `"team_id": 22`. This way, if you filtered events by property using `team_id = 22`, it would display all events captured on that user after the `PostHogSDK.shared.register` call, since they all include the specified Super Property. + +However, please note that this does not store properties against the User, only against their events. To store properties against the User object, you should use `PostHogSDK.shared.identify`. More information on this can be found on the [Sending User Information section](#sending-user-information). + +### Removing stored super properties + +Super properties persist across sessions so you have to explicitly remove them if they are no longer relevant. To stop sending a super property with events, you can use `PostHogSDK.shared.unregister`, like so: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.unregister("team_id") +``` + +This removes the super property and subsequent events will not include it. + +If you are doing this as part of a user logging out, you can instead simply use `PostHogSDK.shared.reset` which clears all super properties and more. + +## Reset after logout + +To reset the user's ID and anonymous ID after logout, call `reset`. See [Identifying users](/docs/product-analytics/identify.md#reset) for the shared reset guidance and iOS example. + +## Group analytics + +Group analytics allows you to associate the events for that person's session with a group (e.g. teams, organizations, etc.). See [Group Analytics](/docs/product-analytics/group-analytics.md) for iOS examples and implementation details. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the [pricing page](/pricing.md). + +## Opt out of data capture + +You can completely opt users out from data capture by default or on a per-person basis. See [Complete opt-out](/docs/product-analytics/privacy.md#complete-opt-out) for iOS examples. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +### Bootstrapping flags + +Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. + +To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones. + +Set `config.bootstrap` before calling `setup()` to seed identity and flag values before the first `/flags` response (requires iOS SDK `3.66.0`+): + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.bootstrap = PostHogBootstrapConfig( + distinctId: "distinct_id_of_your_user", + isIdentifiedId: true, + featureFlags: [ + "flag-1": true, + "variant-flag": "control" + ], + featureFlagPayloads: nil +) +PostHogSDK.shared.setup(config) +``` + +- **Bootstrapped identity applies during setup.** On a fresh install, setting it before `setup()` means events captured synchronously during initialization (like `Application Installed`) carry your distinct ID instead of the SDK-generated UUID. + - An **anonymous** bootstrap (`isIdentifiedId: false`, the default) seeds the anonymous ID only when none is persisted yet. Once an anonymous ID exists on disk, or the person has been identified, the SDK ignores it. + - An **identified** bootstrap (`isIdentifiedId: true`) is for a signed-in identity available to your app (for example, from a backend session token). On a fresh install, it seeds the distinct ID, marks the person identified, and generates a separate device ID. On a returning install, a matching anonymous ID is marked identified without emitting `$identify`; a different anonymous ID is merged via `identify()` when person profiles are enabled. This emits `$identify` unless capturing is opted out. A different, already-identified person is left untouched. +- **Bootstrapped flags are served until the first `/flags` response, then replaced.** A complete `/flags` response takes over entirely, so bootstrapped-only keys don't persist past it. Only *enabled* flags are seeded: a `true` boolean or a non-empty variant string. A `false` or empty value is dropped, matching posthog-js. Seed payloads with the separate `featureFlagPayloads` option. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared on `reset()`. + +The feature-flags-loaded signal fires as soon as bootstrapped flags are applied, so startup logic can read them immediately. These SDKs don't support the `sessionID` bootstrap option. When person profiles are set to `never`, the SDK preserves a different anonymous identity instead of merging it into an identified bootstrap. + +See the [SDK bootstrapping guide](/docs/libraries/bootstrapping.md) for the cross-SDK overview. + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code. See [adding experiment code](/docs/experiments/adding-experiment-code.md) for iOS examples. + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## A note about IDFA (identifier for advertisers) collection in iOS 14 + +Starting with iOS 14, Apple will further restrict apps that track users. Any references to Apple's AdSupport framework, even in strings, [will trip](https://github.com/PostHog/posthog-ios/issues/6) the App Store's static analysis. + +Hence **starting with posthog-ios version 1.2.0** we have removed all references to Apple's AdSupport framework. + +## Session replay + +> **Note:** Session replay is currently only available on iOS. For future macOS support, please follow and upvote [this GitHub issue](https://github.com/PostHog/posthog-ios/issues/200). + +To set up [session replay](/docs/session-replay/mobile.md) in your project, all you need to do is install the iOS SDK, enable "Record user sessions" in [your project settings](https://us.posthog.com/settings/project-replay) and enable the `sessionReplay` option. + +## Surveys + +[Surveys](/docs/surveys.md) launched with [popover presentation](/docs/surveys/creating-surveys.md#presentation) are automatically shown to users matching the [display conditions](/docs/surveys/creating-surveys.md#display-conditions) you set up. + +## Error tracking + +To set up error tracking in your project, see the [error tracking docs](/docs/error-tracking.md). + +## Debug mode + +If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. + +You can enable debug mode by setting the `debug` option to `true` in the `PostHogConfig` object. A common pattern is to set this to `true` in development environments only for local development. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.debug = true +PostHogSDK.shared.setup(config) +``` + +This will enable verbose logs about the inner workings of the SDK. + +You can also toggle debug by calling the `PostHogSDK.shared.debug()` method in your code. + +Swift + +PostHog AI + +```swift +// Enable debug mode +PostHogSDK.shared.debug(true) +// Disable debug mode +PostHogSDK.shared.debug(false) +``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-java/SKILL.md b/skills/posthog/all/skills/feature-flags-java/SKILL.md index a5e99b2a..8a4afebf 100644 --- a/skills/posthog/all/skills/feature-flags-java/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-java/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-java description: PostHog feature flags for Java applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Java @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Java applications. - `references/java.md` - Java feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,4 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op diff --git a/skills/posthog/all/skills/feature-flags-java/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-java/references/COMMANDMENTS.md new file mode 100644 index 00000000..08d1eb78 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-java/references/COMMANDMENTS.md @@ -0,0 +1,5 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op diff --git a/skills/posthog/all/skills/feature-flags-java/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-java/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-java/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-java/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-java/references/best-practices.md b/skills/posthog/all/skills/feature-flags-java/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-java/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-java/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-java/references/java.md b/skills/posthog/all/skills/feature-flags-java/references/java.md index b70bfa66..f248c5f6 100644 --- a/skills/posthog/all/skills/feature-flags-java/references/java.md +++ b/skills/posthog/all/skills/feature-flags-java/references/java.md @@ -1,4 +1,10 @@ -# Java feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Java Feature Flags installation - Docs + +Copy page + +# Java Feature Flags installation - Docs The best way to install the PostHog Java SDK is with a build system like Gradle or Maven. This ensures you can easily upgrade to the latest versions. @@ -85,9 +91,9 @@ PostHogConfig config = PostHogConfig .build(); ``` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-laravel/SKILL.md b/skills/posthog/all/skills/feature-flags-laravel/SKILL.md new file mode 100644 index 00000000..109b21c2 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/SKILL.md @@ -0,0 +1,46 @@ +--- +name: feature-flags-laravel +description: PostHog feature flags for Laravel applications +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for Laravel + +This skill helps you add PostHog feature flags to Laravel applications. + +## Reference files + +- `references/php.md` - Php feature flags installation - docs +- `references/laravel.md` - Laravel - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Create a dedicated PostHogService class in app/Services/ - do NOT scatter PostHog::capture calls throughout controllers +- Register PostHog configuration in config/posthog.php using env() for all settings (api_key, host, disabled) +- Do NOT use Laravel's event system or observers for analytics - call capture explicitly where actions occur +- Call PostHog::flush() after capture in queue jobs, Horizon, and Octane - a long-running worker never destructs, so its events sit in the SDK buffer until batch_size (default 100) is reached and are silently lost on restart +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-laravel/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-laravel/references/COMMANDMENTS.md new file mode 100644 index 00000000..80ab83ad --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Create a dedicated PostHogService class in app/Services/ - do NOT scatter PostHog::capture calls throughout controllers +- Register PostHog configuration in config/posthog.php using env() for all settings (api_key, host, disabled) +- Do NOT use Laravel's event system or observers for analytics - call capture explicitly where actions occur +- Call PostHog::flush() after capture in queue jobs, Horizon, and Octane - a long-running worker never destructs, so its events sit in the SDK buffer until batch_size (default 100) is reached and are silently lost on restart +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-laravel/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-laravel/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-laravel/references/best-practices.md b/skills/posthog/all/skills/feature-flags-laravel/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-laravel/references/laravel.md b/skills/posthog/all/skills/feature-flags-laravel/references/laravel.md new file mode 100644 index 00000000..830063b9 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/references/laravel.md @@ -0,0 +1,176 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Laravel - Docs + +Copy page + +# Laravel - Docs + +PostHog integrates with Laravel through the [PostHog PHP SDK](/docs/libraries/php.md). This page covers Laravel-specific setup. For SDK features such as event capture, identifying users, feature flags, group analytics, and configuration options, see the [PHP SDK docs](/docs/libraries/php.md). + +## Installation + +Install the PHP SDK as described in the [PHP installation guide](/docs/libraries/php.md#installation), then add your project token and host to `.env`: + +.env + +PostHog AI + +```bash +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com +``` + +Add PostHog to Laravel's services config: + +config/services.php + +PostHog AI + +```php +'posthog' => [ + 'api_key' => env('POSTHOG_API_KEY'), + 'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'), +], +``` + +Initialize PostHog in the `boot` method of `app/Providers/AppServiceProvider.php`: + +app/Providers/AppServiceProvider.php + +PostHog AI + +```php + config('services.posthog.host'), + ] + ); + } +} +``` + +## Request context middleware + +Client SDKs such as [PostHog JS](/docs/libraries/js.md) can send tracing headers to your Laravel backend. Configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Laravel backend hostname so browser requests include the session and distinct ID headers. + +The PHP SDK can read `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` headers and apply them to events captured during the request. Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side events or decisions, pass an authenticated `distinctId` explicitly, such as `auth()->id()`. For the lower-level context APIs, see the [PHP request context docs](/docs/libraries/php.md#request-context). + +Add middleware like this: + +app/Http/Middleware/PostHogRequestContext.php + +PostHog AI + +```php +headers->all()); + $context['properties'] = array_merge( + $context['properties'] ?? [], + array_filter([ + '$current_url' => $request->fullUrl(), + '$request_method' => $request->method(), + '$request_path' => $request->getPathInfo(), + '$user_agent' => $request->userAgent(), + '$ip' => $request->ip(), + ], static fn ($value): bool => $value !== null && $value !== '') + ); + return PostHog::withContext( + $context, + static fn (): Response => $next($request), + ['fresh' => true] + ); + } +} +``` + +Register this middleware using your Laravel version's normal middleware registration. + +## Error tracking in Laravel + +The PHP SDK supports [error tracking](/docs/libraries/php.md#error-tracking), but Laravel handles most request exceptions before they become uncaught PHP exceptions. Capture Laravel-reported exceptions explicitly. + +In Laravel 11 and later, add a report callback in `bootstrap/app.php`: + +bootstrap/app.php + +PostHog AI + +```php +use Illuminate\Foundation\Configuration\Exceptions; +use PostHog\PostHog; +use Throwable; +->withExceptions(function (Exceptions $exceptions): void { + $exceptions->report(function (Throwable $e): void { + if (! config('services.posthog.api_key')) { + return; + } + PostHog::captureException( + $e, + auth()->id() !== null ? (string) auth()->id() : null, + [ + '$current_url' => request()->fullUrl(), + '$request_method' => request()->method(), + ] + ); + }); +}) +``` + +For older Laravel versions, call `PostHog::captureException()` from your exception handler's `report` method. + +## Long-running processes + +In normal PHP request lifecycles, queued events flush when the client is destroyed. In long-running Laravel processes such as queue workers, Horizon, or Octane, call `PostHog::flush()` after capturing important events or at the end of a job/request. + +If you prefer immediate delivery in queue workers, configure the PHP SDK with `batch_size` set to `1` for those workers: + +PHP + +PostHog AI + +```php +PostHog::init( + '', + [ + 'host' => config('services.posthog.host'), + 'batch_size' => 1, + ] +); +``` + +## Next steps + +See the [PHP SDK docs](/docs/libraries/php.md) for usage examples and the full API reference. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-laravel/references/php.md b/skills/posthog/all/skills/feature-flags-laravel/references/php.md new file mode 100644 index 00000000..37699363 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-laravel/references/php.md @@ -0,0 +1,193 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Feature Flags installation - Docs + +Copy page + +# PHP Feature Flags installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog PHP library using Composer: + + Terminal + + PostHog AI + + ```bash + composer require posthog/posthog-php + ``` + +2. 2 + + ## Configure PostHog + + Required + + Initialize the PostHog client with your project token and host: + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + ['host' => 'https://us.i.posthog.com'] + ); + ``` + +3. 3 + + ## Send events + + Recommended + + Once installed, you can manually send events to test your integration: + + PHP + + PostHog AI + + ```php + PostHog::capture([ + 'distinctId' => 'test-user', + 'event' => 'test-event', + ]); + ``` + +4. 4 + + ## Evaluate boolean feature flags + + Required + + Check if a feature flag is enabled: + + ```php + $isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') + if ($isMyFlagEnabledForUser) { + // Do something differently for this user + } + ``` + +5. 5 + + ## Evaluate multivariate feature flags + + Optional + + For multivariate flags, check which variant the user has been assigned: + + ```php + $enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') + if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant + # Do something differently for this user + } + ``` + +6. 6 + + ## Include feature flag information in events + + Required + + If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + + **Note:** This step is only required for events captured using our server-side SDKs or API. + + ## Set send_feature_flags (recommended) + + Set `send_feature_flags` to `true` in your capture call: + + PHP + + PostHog AI + + ```php + PostHog::capture(array( + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'send_feature_flags' => true + )); + ``` + + ## Include $feature property + + Include the `$feature/feature_flag_name` property in your event properties: + + PHP + + PostHog AI + + ```php + PostHog::capture(array( + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => array( + '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + ) + )); + ``` + +7. 7 + + ## Override server properties + + Optional + + Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with: + + ```php + PostHog::getFeatureFlag( + 'flag-key', + 'distinct_id_of_the_user', + [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id' + ], // groups + ['property_name' => 'value'], // person properties + [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'] + ], // group properties + false, // onlyEvaluateLocally, Optional. Defaults to false. + true // sendFeatureFlagEvents + ) + ``` + +8. 8 + + ## Running experiments + + Optional + + Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard. + +9. 9 + + ## Next steps + + Recommended + + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. + + | Resource | Description | + | --- | --- | + | [Creating a feature flag](/docs/feature-flags/creating-feature-flags.md) | How to create a feature flag in PostHog | + | [Adding feature flag code](/docs/feature-flags/adding-feature-flag-code.md) | How to check flags in your code for all platforms | + | [Framework-specific guides](/docs/feature-flags/tutorials.md#framework-guides) | Setup guides for React Native, Next.js, Flutter, and other frameworks | + | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | + | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-nextjs/SKILL.md b/skills/posthog/all/skills/feature-flags-nextjs/SKILL.md index ff1b0f91..aaf7a774 100644 --- a/skills/posthog/all/skills/feature-flags-nextjs/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-nextjs/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-nextjs description: PostHog feature flags for Next.js applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Next.js @@ -15,7 +15,8 @@ This skill helps you add PostHog feature flags to Next.js applications. - `references/react.md` - React feature flags installation - docs - `references/next-js.md` - Next.js - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -32,12 +33,14 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup - The PostHog React hooks (useFeatureFlagEnabled, useFeatureFlagPayload) work WITHOUT PostHogProvider if posthog-js is already initialized (e.g., via instrumentation-client.ts) - In client components, import and use hooks directly - the React context defaults to the posthog-js singleton - Do NOT wrap components in PostHogProvider just for feature flags - it's unnecessary if posthog-js is initialized globally - 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 - For flags that affect initial render, evaluate server-side and pass as props to prevent UI flicker - Client-side hooks may return undefined initially while flags load - handle this loading state @@ -49,3 +52,6 @@ Check if a PostHog MCP server is connected. If available, look for tools related - Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler - To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect - useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-nextjs/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-nextjs/references/COMMANDMENTS.md new file mode 100644 index 00000000..4ede2940 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-nextjs/references/COMMANDMENTS.md @@ -0,0 +1,26 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For Next.js 15.3+, initialize PostHog in instrumentation-client.ts for the simplest setup +- The PostHog React hooks (useFeatureFlagEnabled, useFeatureFlagPayload) work WITHOUT PostHogProvider if posthog-js is already initialized (e.g., via instrumentation-client.ts) +- In client components, import and use hooks directly - the React context defaults to the posthog-js singleton +- Do NOT wrap components in PostHogProvider just for feature flags - it's unnecessary if posthog-js is initialized globally +- 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 +- For flags that affect initial render, evaluate server-side and pass as props to prevent UI flicker +- Client-side hooks may return undefined initially while flags load - handle this loading state +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-nextjs/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-nextjs/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-nextjs/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-nextjs/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nextjs/references/best-practices.md b/skills/posthog/all/skills/feature-flags-nextjs/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-nextjs/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-nextjs/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nextjs/references/next-js.md b/skills/posthog/all/skills/feature-flags-nextjs/references/next-js.md index 4e178d1c..13c9c804 100644 --- a/skills/posthog/all/skills/feature-flags-nextjs/references/next-js.md +++ b/skills/posthog/all/skills/feature-flags-nextjs/references/next-js.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Next.js - Docs + +Copy page + # Next.js - Docs PostHog makes it easy to get data about traffic and usage of your [Next.js](https://nextjs.org/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more. @@ -21,7 +27,7 @@ To follow this guide along, you need: Install PostHog for Next.js in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. -`npx @posthog/wizard@latest` +`npx @posthog/wizard` [Learn more](/wizard.md) @@ -57,6 +63,18 @@ pnpm add posthog-js bun add posthog-js ``` +> **If your site sets a Content-Security-Policy**, it needs to allow PostHog. This applies to the snippet and to package installs alike: the SDK lazy-loads extra bundles (session replay, surveys) from PostHog's CDN, and sends events to the ingestion host. PostHog serves from subdomains of `posthog.com` that change over time, so allow the wildcard: +> +> PostHog AI +> +> ``` +> script-src 'self' https://*.posthog.com; +> connect-src 'self' https://*.posthog.com; +> worker-src 'self' blob: data:; +> ``` +> +> `script-src` covers the snippet and the lazy-loaded bundles, `connect-src` covers event ingestion and feature flags, and `worker-src` covers session replay. The [toolbar needs a few more](/docs/advanced/content-security-policy.md), or use a [reverse proxy](/docs/advanced/proxy.md) so everything is first-party. Failing to do so causes silent failures where `capture` and `identify` calls never send, so the integration looks complete while zero events arrive. Remember `connect-src` falls back to `default-src`, so `default-src 'self'` blocks event delivery even when the script itself is bundled. + Add your environment variables to your `.env.local` file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project token in your [project settings](https://app.posthog.com/project/settings). .env.local @@ -64,7 +82,7 @@ Add your environment variables to your `.env.local` file and to your hosting pro PostHog AI ```shell -NEXT_PUBLIC_POSTHOG_TOKEN= +NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN= NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com ``` @@ -80,9 +98,9 @@ PostHog AI ```javascript import posthog from 'posthog-js' -posthog.init(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, { +posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30' + defaults: '2026-05-30' }); ``` @@ -90,9 +108,9 @@ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, { ```typescript import posthog from 'posthog-js' -posthog.init(process.env.NEXT_PUBLIC_POSTHOG_TOKEN!, { +posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30' + defaults: '2026-05-30' }); ``` @@ -113,8 +131,34 @@ See the [bootstrapping guide](/docs/feature-flags/bootstrapping.md) for more inf > **Identifying users is required.** Call `posthog.identify('your-user-id')` after login to link events to a known user. This is what connects frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), and [error tracking](/docs/error-tracking.md) to the same person — and lets backend events link back too. > +> Use a stable ID from your auth system when possible, not an email or display name. Send those as person properties instead. If your app has no other stable key, email works as a fallback if they are unique. Never a shared literal like `"anonymous"` or `"user"`, which pools many people onto one person and corrupts their data. When no ID is available at all, skip the identify and retain the anonymous distinct ID that's automatically assigned. +> +> Call `posthog.reset()` on logout, so the next person to use the browser doesn't inherit the last one's identity. +> > See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. +### Linking client and server events + +Next.js apps usually capture on both sides. To keep them on the same person, use the same distinct ID in both, and let the browser tell your server which one that is. + +If your app calls your own backend, `tracing_headers` adds `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` to matching `fetch` and `XMLHttpRequest` requests. This lets server-side SDKs link backend events, errors, and LLM traces back to frontend sessions and replays. Use hostnames only, without protocols or paths. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + // Optional: send PostHog session/user context to your backend + tracing_headers: ['api.example.com'], +}) +``` + +This works in local development too, but match on the hostname alone: use `'localhost'`, not `'localhost:3000'`. Ports are never part of a hostname, so a value with one in it never matches anything. `localhost` and `127.0.0.1` are also different hostnames — use whichever your app actually calls. + +Tracing headers help you attribute events across front and backend consistently. When this isn't available, use your server-side stable IDs to deduce the matching `distinctId`, and pass it in when capturing the event. + Set up a reverse proxy (recommended) We recommend [setting up a reverse proxy](/docs/advanced/proxy.md), so that events are less likely to be intercepted by tracking blockers. @@ -131,7 +175,7 @@ This makes it possible to track users across their entire journey (e.g. from vis Add IPs to Firewall/WAF allowlists (recommended) -For certain features like [heatmaps](/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site. +For certain features like [heatmaps](/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog's requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site. **EU**: `3.75.65.221`, `18.197.246.42`, `3.120.223.253` @@ -148,14 +192,12 @@ JavaScript PostHog AI ```javascript -'use client' -import posthog from 'posthog-js' +"use client"; +import posthog from "posthog-js"; export default function Home() { return (
- +
); } @@ -170,11 +212,11 @@ JavaScript PostHog AI ```javascript -'use client' -import { useFeatureFlagEnabled } from 'posthog-js/react' +"use client"; +import { useFeatureFlagEnabled } from "@posthog/react"; export default function FeatureComponent() { - const showNewFeature = useFeatureFlagEnabled('new-feature') - return showNewFeature ? : + const showNewFeature = useFeatureFlagEnabled("new-feature"); + return showNewFeature ? : ; } ``` @@ -185,7 +227,7 @@ See the [React SDK docs](/docs/libraries/react.md) for examples of how to use: - [`posthog-js` functions like custom event capture, user identification, and more.](/docs/libraries/react.md#using-posthog-js-functions) - [Feature flags including variants and payloads.](/docs/libraries/react.md#feature-flags) -You can also read [the full `posthog-js` documentation](/docs/libraries/js/features.md) for all the usable functions. +You can also read [the full `posthog-js` documentation](/docs/libraries/js/usage.md) for all the usable functions. ## Server-side analytics @@ -235,7 +277,7 @@ PostHog AI // app/posthog.js import { PostHog } from 'posthog-node' export default function PostHogClient() { - const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, { + const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, flushAt: 1, flushInterval: 0 @@ -290,6 +332,7 @@ PostHog AI // pages/posts/[id].js import { useContext, useEffect, useState } from 'react' import { getServerSession } from "next-auth/next" +import { authOptions } from '@/lib/auth' import { PostHog } from 'posthog-node' export default function Post({ post, flags }) { const [ctaState, setCtaState] = useState() @@ -311,18 +354,21 @@ export default function Post({ post, flags }) { ) } export async function getServerSideProps(ctx) { - const session = await getServerSession(ctx.req, ctx.res) + // Pass authOptions, or your session callbacks don't run. + const session = await getServerSession(ctx.req, ctx.res, authOptions) let flags = null if (session) { const client = new PostHog( - process.env.NEXT_PUBLIC_POSTHOG_TOKEN, + process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, } ) - flags = await client.getAllFlags(session.user.email); + // A stable ID from your auth system, not an email. See the note below. + const distinctId = session.user.id + flags = await client.getAllFlags(distinctId); client.capture({ - distinctId: session.user.email, + distinctId, event: 'loaded blog article', properties: { $current_url: ctx.req.url, @@ -341,6 +387,28 @@ export async function getServerSideProps(ctx) { } ``` +> **Note**: next-auth doesn't put a user ID on the session by default. Its session is `{ name, email, image }`, so `session.user.id` is `undefined` until you add it yourself with a session callback in your `authOptions`: +> +> JavaScript +> +> PostHog AI +> +> ```javascript +> // lib/auth.js +> export const authOptions = { +> callbacks: { +> session({ session, token, user }) { +> // JWT sessions (the default) carry the user ID in token.sub. +> // Database sessions get it from user.id instead. +> session.user.id = token?.sub ?? user.id +> return session +> }, +> }, +> } +> ``` +> +> Capturing with an `undefined` distinct ID creates events that belong to nobody, so check that the ID arrives before relying on it. + > **Note**: Make sure to *always* call `await client.shutdown()` after sending events from the server-side. PostHog queues events into larger batches, and this call forces all batched events to be flushed immediately. ### Server-side configuration @@ -354,7 +422,7 @@ TSX PostHog AI ```jsx -posthog.init(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, { +posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, { // ... your configuration fetch_options: { cache: 'force-cache', // Use Next.js cache @@ -376,9 +444,9 @@ To improve the reliability of client-side tracking and make requests less likely - [How to set up Next.js pages router analytics, feature flags, and more](/tutorials/nextjs-pages-analytics.md) - [How to set up Next.js A/B tests](/tutorials/nextjs-ab-tests.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nextjs/references/react.md b/skills/posthog/all/skills/feature-flags-nextjs/references/react.md index 97e88572..91d86ce0 100644 --- a/skills/posthog/all/skills/feature-flags-nextjs/references/react.md +++ b/skills/posthog/all/skills/feature-flags-nextjs/references/react.md @@ -1,4 +1,10 @@ -# React feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# React Feature Flags installation - Docs + +Copy page + +# React Feature Flags installation - Docs 1. 1 @@ -34,15 +40,15 @@ Required - Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_PUBLIC_` prefix: + Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_` prefix to expose them to the client: .env PostHog AI ```bash - VITE_PUBLIC_POSTHOG_PROJECT_TOKEN= - VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + VITE_POSTHOG_PROJECT_TOKEN= + VITE_POSTHOG_HOST=https://us.i.posthog.com ``` 3. 3 @@ -64,12 +70,12 @@ import App from './App.jsx' import { PostHogProvider } from '@posthog/react' const options = { - api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30', + api_host: import.meta.env.VITE_POSTHOG_HOST, + defaults: '2026-05-30', } as const createRoot(document.getElementById('root')).render( - + @@ -293,9 +299,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nodejs/SKILL.md b/skills/posthog/all/skills/feature-flags-nodejs/SKILL.md index 48c0fdcd..ee861c00 100644 --- a/skills/posthog/all/skills/feature-flags-nodejs/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-nodejs/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-nodejs description: PostHog feature flags for Node.js applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Node.js @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Node.js applications. - `references/nodejs.md` - Node.js feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-nodejs/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-nodejs/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-nodejs/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-nodejs/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-nodejs/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-nodejs/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-nodejs/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nodejs/references/best-practices.md b/skills/posthog/all/skills/feature-flags-nodejs/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-nodejs/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-nodejs/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-nodejs/references/nodejs.md b/skills/posthog/all/skills/feature-flags-nodejs/references/nodejs.md index 788295e7..d7522f26 100644 --- a/skills/posthog/all/skills/feature-flags-nodejs/references/nodejs.md +++ b/skills/posthog/all/skills/feature-flags-nodejs/references/nodejs.md @@ -1,4 +1,10 @@ -# Node.js feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Node.js Feature Flags installation - Docs + +Copy page + +# Node.js Feature Flags installation - Docs 1. 1 @@ -207,9 +213,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-php/SKILL.md b/skills/posthog/all/skills/feature-flags-php/SKILL.md index 6903b39e..75c9e5e5 100644 --- a/skills/posthog/all/skills/feature-flags-php/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-php/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-php description: PostHog feature flags for PHP applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for PHP @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to PHP applications. - `references/php.md` - Php feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,8 +32,10 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Remember that source code is available in the vendor directory after composer install - posthog/posthog-php is the PHP SDK package name - Check composer.json for existing dependencies and autoload configuration before adding new files - The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() - PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-php/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-php/references/COMMANDMENTS.md new file mode 100644 index 00000000..71c9848e --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-php/references/COMMANDMENTS.md @@ -0,0 +1,11 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-php/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-php/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-php/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-php/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-php/references/best-practices.md b/skills/posthog/all/skills/feature-flags-php/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-php/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-php/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-php/references/php.md b/skills/posthog/all/skills/feature-flags-php/references/php.md index acf0ed41..37699363 100644 --- a/skills/posthog/all/skills/feature-flags-php/references/php.md +++ b/skills/posthog/all/skills/feature-flags-php/references/php.md @@ -1,4 +1,10 @@ -# PHP feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Feature Flags installation - Docs + +Copy page + +# PHP Feature Flags installation - Docs 1. 1 @@ -178,9 +184,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-python/SKILL.md b/skills/posthog/all/skills/feature-flags-python/SKILL.md index ebf9523e..4ea5c45f 100644 --- a/skills/posthog/all/skills/feature-flags-python/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-python/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-python description: PostHog feature flags for Python applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Python @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Python applications. - `references/python.md` - Python feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Remember that source code is available in the venv/site-packages directory - posthog is the Python SDK package name - Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands diff --git a/skills/posthog/all/skills/feature-flags-python/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-python/references/COMMANDMENTS.md new file mode 100644 index 00000000..c8ef6d3b --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-python/references/COMMANDMENTS.md @@ -0,0 +1,15 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context diff --git a/skills/posthog/all/skills/feature-flags-python/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-python/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-python/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-python/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-python/references/best-practices.md b/skills/posthog/all/skills/feature-flags-python/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-python/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-python/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-python/references/python.md b/skills/posthog/all/skills/feature-flags-python/references/python.md index e7f631dc..30634ac5 100644 --- a/skills/posthog/all/skills/feature-flags-python/references/python.md +++ b/skills/posthog/all/skills/feature-flags-python/references/python.md @@ -1,4 +1,10 @@ -# Python feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Python Feature Flags installation - Docs + +Copy page + +# Python Feature Flags installation - Docs 1. 1 @@ -56,7 +62,7 @@ ```python import posthog - posthog.capture('user_123', 'user_signed_up', properties={'example_property': 'example_value'}) + posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'}) ``` 4. 4 @@ -182,9 +188,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react-native/SKILL.md b/skills/posthog/all/skills/feature-flags-react-native/SKILL.md index 3831b4fc..28857f3c 100644 --- a/skills/posthog/all/skills/feature-flags-react-native/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-react-native/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-react-native description: PostHog feature flags for React Native applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for React Native @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to React Native applications. - `references/react-native.md` - React native feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically - Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes - Do NOT use useEffect for data transformation - calculate derived values during render instead @@ -43,3 +45,6 @@ Check if a PostHog MCP server is connected. If available, look for tools related - Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) - react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it - Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-react-native/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-react-native/references/COMMANDMENTS.md new file mode 100644 index 00000000..c8bc30a4 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-react-native/references/COMMANDMENTS.md @@ -0,0 +1,20 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- posthog-react-native is the React Native SDK package name +- Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime) +- react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it +- Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-react-native/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-react-native/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-react-native/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-react-native/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react-native/references/best-practices.md b/skills/posthog/all/skills/feature-flags-react-native/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-react-native/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-react-native/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react-native/references/react-native.md b/skills/posthog/all/skills/feature-flags-react-native/references/react-native.md index 6d695950..1a83c7f0 100644 --- a/skills/posthog/all/skills/feature-flags-react-native/references/react-native.md +++ b/skills/posthog/all/skills/feature-flags-react-native/references/react-native.md @@ -1,4 +1,10 @@ -# React Native feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# React Native Feature Flags installation - Docs + +Copy page + +# React Native Feature Flags installation - Docs 1. 1 @@ -105,7 +111,7 @@ if (isMyFlagEnabled) { // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload } return ... } @@ -127,7 +133,7 @@ if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload } return ... } @@ -157,9 +163,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react/SKILL.md b/skills/posthog/all/skills/feature-flags-react/SKILL.md index 61eb73eb..6d727e4c 100644 --- a/skills/posthog/all/skills/feature-flags-react/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-react/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-react description: PostHog feature flags for React applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for React @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to React applications. - `references/react.md` - React feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically - Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes - Do NOT use useEffect for data transformation - calculate derived values during render instead @@ -39,3 +41,6 @@ Check if a PostHog MCP server is connected. If available, look for tools related - Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler - To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect - useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-react/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-react/references/COMMANDMENTS.md new file mode 100644 index 00000000..cc803992 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-react/references/COMMANDMENTS.md @@ -0,0 +1,16 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically +- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes +- Do NOT use useEffect for data transformation - calculate derived values during render instead +- Do NOT use useEffect to respond to user events - put that logic in the event handler itself +- Do NOT use useEffect to chain state updates - calculate all related updates together in the event handler +- Do NOT use useEffect to notify parent components - call the parent callback alongside setState in the event handler +- To reset component state when a prop changes, pass the prop as the component's key instead of using useEffect +- useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions) +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-react/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-react/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-react/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-react/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react/references/best-practices.md b/skills/posthog/all/skills/feature-flags-react/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-react/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-react/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-react/references/react.md b/skills/posthog/all/skills/feature-flags-react/references/react.md index 97e88572..91d86ce0 100644 --- a/skills/posthog/all/skills/feature-flags-react/references/react.md +++ b/skills/posthog/all/skills/feature-flags-react/references/react.md @@ -1,4 +1,10 @@ -# React feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# React Feature Flags installation - Docs + +Copy page + +# React Feature Flags installation - Docs 1. 1 @@ -34,15 +40,15 @@ Required - Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_PUBLIC_` prefix: + Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the `VITE_` prefix to expose them to the client: .env PostHog AI ```bash - VITE_PUBLIC_POSTHOG_PROJECT_TOKEN= - VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + VITE_POSTHOG_PROJECT_TOKEN= + VITE_POSTHOG_HOST=https://us.i.posthog.com ``` 3. 3 @@ -64,12 +70,12 @@ import App from './App.jsx' import { PostHogProvider } from '@posthog/react' const options = { - api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, - defaults: '2026-01-30', + api_host: import.meta.env.VITE_POSTHOG_HOST, + defaults: '2026-05-30', } as const createRoot(document.getElementById('root')).render( - + @@ -293,9 +299,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/SKILL.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/SKILL.md new file mode 100644 index 00000000..965cb0dc --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/SKILL.md @@ -0,0 +1,54 @@ +--- +name: feature-flags-ruby-on-rails +description: PostHog feature flags for Ruby on Rails applications +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for Ruby on Rails + +This skill helps you add PostHog feature flags to Ruby on Rails applications. + +## Reference files + +- `references/ruby.md` - Ruby feature flags installation - docs +- `references/ruby-on-rails.md` - Ruby on rails - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use posthog-rails gem alongside posthog-ruby for automatic exception capture and ActiveJob instrumentation +- Run `rails generate posthog:install` to create the initializer, or manually create config/initializers/posthog.rb +- Configure auto_capture_exceptions: true to automatically track unhandled exceptions in controllers +- Configure report_rescued_exceptions: true to also capture exceptions that Rails rescues (e.g. with rescue_from) +- Configure auto_instrument_active_job: true to track background job failures with job class, queue, and arguments +- Use PostHog.capture() and PostHog.identify() class-level methods (NOT instance methods) — the posthog-rails gem manages the client lifecycle via PostHog.init +- Do NOT manually create PostHog::Client instances in Rails — use PostHog.init in the initializer and PostHog.capture/identify everywhere else +- capture_exception takes POSITIONAL args: PostHog.capture_exception(exception, distinct_id, additional_properties) — do NOT use keyword args +- Define posthog_distinct_id on the User model for automatic user association in error reports — posthog-rails auto-detects by trying: posthog_distinct_id, distinct_id, id, pk, uuid (in order) +- For ActiveJob user association, use the class-level DSL `posthog_distinct_id ->(user) { user.email }` or pass user_id: in a hash argument +- Store the project token in Rails credentials or environment variables, never hardcode +- For frontend tracking alongside posthog-rails, add the posthog-js snippet to the layout template — posthog-js handles pageviews, session replay, and client-side errors while posthog-ruby handles backend events, server errors, feature flags, and background jobs +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/COMMANDMENTS.md new file mode 100644 index 00000000..9f024f48 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/COMMANDMENTS.md @@ -0,0 +1,23 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use posthog-rails gem alongside posthog-ruby for automatic exception capture and ActiveJob instrumentation +- Run `rails generate posthog:install` to create the initializer, or manually create config/initializers/posthog.rb +- Configure auto_capture_exceptions: true to automatically track unhandled exceptions in controllers +- Configure report_rescued_exceptions: true to also capture exceptions that Rails rescues (e.g. with rescue_from) +- Configure auto_instrument_active_job: true to track background job failures with job class, queue, and arguments +- Use PostHog.capture() and PostHog.identify() class-level methods (NOT instance methods) — the posthog-rails gem manages the client lifecycle via PostHog.init +- Do NOT manually create PostHog::Client instances in Rails — use PostHog.init in the initializer and PostHog.capture/identify everywhere else +- capture_exception takes POSITIONAL args: PostHog.capture_exception(exception, distinct_id, additional_properties) — do NOT use keyword args +- Define posthog_distinct_id on the User model for automatic user association in error reports — posthog-rails auto-detects by trying: posthog_distinct_id, distinct_id, id, pk, uuid (in order) +- For ActiveJob user association, use the class-level DSL `posthog_distinct_id ->(user) { user.email }` or pass user_id: in a hash argument +- Store the project token in Rails credentials or environment variables, never hardcode +- For frontend tracking alongside posthog-rails, add the posthog-js snippet to the layout template — posthog-js handles pageviews, session replay, and client-side errors while posthog-ruby handles backend events, server errors, feature flags, and background jobs +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/best-practices.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby-on-rails.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby-on-rails.md new file mode 100644 index 00000000..74838eaf --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby-on-rails.md @@ -0,0 +1,610 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby on Rails - Docs + +Copy page + +# Ruby on Rails - Docs + +PostHog makes it easy to get data about traffic and usage of your Ruby on Rails app. Integrating PostHog enables analytics, custom event capture, feature flags, and automatic exception tracking. + +This guide walks you through integrating PostHog into your Rails app using the [posthog-rails gem](https://github.com/PostHog/posthog-ruby/tree/main/posthog-rails). + +## Beta: integration via LLM + +Install PostHog for Rails in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. + +`npx @posthog/wizard` + +[Learn more](/wizard.md) + +Or, to integrate manually, continue with the rest of this guide. + +## Features + +- **Automatic exception tracking** – Captures unhandled and rescued exceptions +- **ActiveJob instrumentation** – Tracks background job exceptions +- **User context** – Automatically associates exceptions with the current user +- **Smart filtering** – Excludes common Rails exceptions (404s, etc.) by default +- **Request context** – Adds request metadata and optional PostHog tracing header identity/session context to captured events +- **Rails 7.0+ error reporter** – Integrates with Rails' built-in error reporting +- **Log forwarding** – Optionally forwards `Rails.logger` output to [PostHog Logs](/docs/logs.md) over OpenTelemetry, automatically correlated with request context (Ruby 3.3+) + +## Installation + +Add both gems to your Gemfile: + +Gemfile + +PostHog AI + +```ruby +gem 'posthog-ruby', require: 'posthog' +gem 'posthog-rails' +``` + +Then run: + +Terminal + +PostHog AI + +```bash +bundle install +``` + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +### Generate the initializer + +Run the install generator to create the PostHog initializer: + +Terminal + +PostHog AI + +```bash +rails generate posthog:install +``` + +This creates `config/initializers/posthog.rb` with sensible defaults and documentation. + +## Configuration + +`PostHog.init` creates a single client instance used across your app. Avoid creating multiple `PostHog::Client` instances with the same API key, as this can cause dropped events and inconsistent behavior. + +The generated initializer includes the most common options: + +config/initializers/posthog.rb + +PostHog AI + +```ruby +# Rails-specific configuration +PostHog::Rails.configure do |config| + config.auto_capture_exceptions = true # Enable automatic exception capture (default: false) + config.report_rescued_exceptions = true # Report exceptions Rails rescues (default: false) + config.auto_instrument_active_job = true # Instrument background jobs (default: false) + config.use_tracing_headers = true # Use PostHog tracing headers for identity/session context (default: true) + config.capture_user_context = true # Include authenticated user info in exceptions (default: true) + config.current_user_method = :current_user # Method to get current user (default: :current_user) + config.user_id_method = nil # Method to get ID from user object (default: auto-detect) + # Add additional exceptions to ignore + config.excluded_exceptions = ['MyCustomError'] +end +# Core PostHog client initialization +PostHog.init do |config| + # Required: Your PostHog project API key + config.api_key = '' + # Optional: Your PostHog instance URL + config.host = 'https://us.i.posthog.com' + # Optional: Personal API key for feature flags + config.personal_api_key = 'phx_xxxxxxxxx' + # Maximum number of events to queue before dropping (default: 10000) + config.max_queue_size = 10_000 + # Send events synchronously on the calling thread (default: false) + config.sync_mode = false + # Feature flags polling interval in seconds (default: 30) + config.feature_flags_polling_interval = 30 + # Feature flag request timeout in seconds (default: 3) + config.feature_flag_request_timeout_seconds = 3 + # Error callback to detect misconfiguration + config.on_error = proc { |status, msg| + Rails.logger.error("PostHog error: #{msg}") + } + # Before-send callback to modify or drop events + config.before_send = proc { |event| + event[:properties] ||= {} + event[:properties]['environment'] = Rails.env + event + } + # Disable network calls in test mode + config.test_mode = true if Rails.env.test? +end +``` + +You can find your project token and instance address in [your project settings](https://us.posthog.com/project/settings). + +> **Tip:** Use [`Rails.application.credentials`](https://guides.rubyonrails.org/security.html#custom-credentials) to avoid hardcoding API keys. First, add your keys and then reference them in your initializer: +> +> Terminal +> +> PostHog AI +> +> ```bash +> rails credentials:edit +> ``` +> +> config/credentials.yml.enc +> +> PostHog AI +> +> ```yaml +> posthog: +> api_key: +> host: https://us.i.posthog.com +> personal_api_key: phx_xxxxxxxxx +> ``` +> +> config/initializers/posthog.rb +> +> PostHog AI +> +> ```ruby +> config.api_key = Rails.application.credentials.posthog[:api_key] +> config.host = Rails.application.credentials.posthog[:host] +> config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +> ``` + +## Capturing events + +Track custom events anywhere in your Rails app: + +Ruby + +PostHog AI + +```ruby +PostHog.capture({ + distinct_id: current_user.id, + event: 'post_created', + properties: { title: @post.title } +}) +``` + +Identify a user and set their person properties: + +Ruby + +PostHog AI + +```ruby +PostHog.identify({ + distinct_id: current_user.id, + properties: { + email: current_user.email, + plan: current_user.plan + } +}) +``` + +The Rails integration delegates methods like `capture`, `identify`, `alias`, `group_identify`, `evaluate_flags`, `capture_exception`, `flush`, and `shutdown` to the initialized `PostHog::Client`. + +## Request context + +PostHog Rails automatically applies request-scoped context to events captured during web requests. Request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip` is added to event properties. + +When `use_tracing_headers` is enabled, PostHog tracing headers (`X-PostHog-Distinct-Id` and `X-PostHog-Session-Id`) are also used as default `distinct_id` and `$session_id` values. Explicit `distinct_id` and properties passed to `PostHog.capture` always take precedence. + +If you're using [PostHog JS](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Rails backend hostname so browser requests include the session and distinct ID headers. + +Tracing headers are client-controlled analytics context, not authentication or authorization. Pass an authenticated `distinct_id` explicitly for security-sensitive server-side decisions. + +Disable tracing header identity/session capture if you do not want client-supplied tracing headers used for server-side events. Request metadata is still captured: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.use_tracing_headers = false +``` + +## Logs + +To set up [PostHog Logs](/docs/logs.md) in your Rails app, follow the [Ruby on Rails logs installation guide](/docs/logs/installation/ruby-on-rails.md). The integration forwards `Rails.logger` output to PostHog Logs over OpenTelemetry, automatically correlated with each request's distinct ID and session ID. Requires Ruby 3.3+. + +## Error tracking + +For full details on setting up error tracking with Rails, see our [Rails error tracking installation guide](/docs/error-tracking/installation/ruby-on-rails.md). + +### Automatic exception tracking + +When `auto_capture_exceptions` is enabled, exceptions are automatically captured: + +Ruby + +PostHog AI + +```ruby +class PostsController < ApplicationController + def show + @post = Post.find(params[:id]) + # Any exception here is automatically captured + end +end +``` + +`report_rescued_exceptions` controls whether exceptions Rails rescues (for example, exceptions rendered by Rails error pages) are captured. Enable it along with `auto_capture_exceptions` for complete error visibility, or leave it disabled to capture only unhandled exceptions. + +### Manual exception capture + +You can also manually capture exceptions: + +Ruby + +PostHog AI + +```ruby +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' } +) +``` + +If you evaluated feature flags for the request, pass the same snapshot to include matching flag properties on the exception event: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture_exception( + exception, + current_user.id, + { custom_property: 'value' }, + flags: flags +) +``` + +### Background job exceptions + +When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured with job context: + +Ruby + +PostHog AI + +```ruby +class EmailJob < ApplicationJob + def perform(user_id) + user = User.find(user_id) + UserMailer.welcome(user).deliver_now + # Exceptions are automatically captured + end +end +``` + +#### Associating jobs with users + +By default, PostHog extracts a `distinct_id` from job arguments by looking for a `user_id` key in hash arguments: + +Ruby + +PostHog AI + +```ruby +# PostHog will automatically use options[:user_id] as the distinct_id +ProcessOrderJob.perform_later(order.id, user_id: current_user.id) +``` + +For more control, use the `posthog_distinct_id` class method. The proc or block receives the same arguments as `perform`: + +Ruby + +PostHog AI + +```ruby +class SendWelcomeEmailJob < ApplicationJob + posthog_distinct_id ->(user, _options) { user.id } + def perform(user, options = {}) + UserMailer.welcome(user).deliver_now + end +end +``` + +You can also use a block: + +Ruby + +PostHog AI + +```ruby +class ProcessOrderJob < ApplicationJob + posthog_distinct_id do |_order, notify_user_id| + notify_user_id + end + def perform(order, notify_user_id) + # Process the order... + end +end +``` + +### Rails 7.0+ error reporter + +PostHog integrates with Rails' built-in error reporting: + +Ruby + +PostHog AI + +```ruby +# These errors are automatically sent to PostHog +Rails.error.handle do + # Code that might raise an error +end +Rails.error.record(exception, context: { user_id: current_user.id }) +``` + +PostHog automatically extracts the user's distinct ID from `user_id` or `distinct_id` in the context hash. Other context keys are included as properties on the exception event. + +### User context + +PostHog Rails automatically captures authenticated user information from your controllers for exceptions. Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity. + +If your user method has a different name, configure it: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.current_user_method = :logged_in_user +``` + +#### User ID extraction + +By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order: + +1. `posthog_distinct_id` – Define this on your User model for full control +2. `distinct_id` – Common analytics convention +3. `id` – Standard ActiveRecord primary key +4. `pk` – Primary key alias +5. `uuid` – For UUID-based primary keys + +It also checks hash-like users for `id`, `pk`, and `uuid` keys. + +You can configure a specific method: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.user_id_method = :email +``` + +Or define a method on your User model: + +Ruby + +PostHog AI + +```ruby +class User < ApplicationRecord + def posthog_distinct_id + "user_#{id}" # or external_id, or any unique identifier + end +end +``` + +### Excluded exceptions + +The following exceptions are not reported by default (common 4xx errors): + +- `AbstractController::ActionNotFound` +- `ActionController::BadRequest` +- `ActionController::InvalidAuthenticityToken` +- `ActionController::InvalidCrossOriginRequest` +- `ActionController::MethodNotAllowed` +- `ActionController::NotImplemented` +- `ActionController::ParameterMissing` +- `ActionController::RoutingError` +- `ActionController::UnknownFormat` +- `ActionController::UnknownHttpMethod` +- `ActionDispatch::Http::Parameters::ParseError` +- `ActiveRecord::RecordNotFound` +- `ActiveRecord::RecordNotUnique` + +Add more with: + +Ruby + +PostHog AI + +```ruby +PostHog::Rails.config.excluded_exceptions = ['MyException'] +``` + +## Feature flags + +Evaluate flags once for the current user, then read values from the returned snapshot: + +Ruby + +PostHog AI + +```ruby +class PostsController < ApplicationController + def show + flags = PostHog.evaluate_flags(current_user.id) + if flags.enabled?('new-post-design') + render 'posts/show_new' + else + render 'posts/show' + end + end +end +``` + +For multivariate flags and experiments, use `get_flag`: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +variant = flags.get_flag('checkout-experiment') +if variant == 'test' + # Do something differently +end +``` + +When capturing an event after branching on a flag, pass the same `flags` snapshot so the event includes the exact flag values used by your code: + +Ruby + +PostHog AI + +```ruby +flags = PostHog.evaluate_flags(current_user.id) +PostHog.capture({ + distinct_id: current_user.id, + event: 'checkout_started', + flags: flags.only_accessed +}) +``` + +For local evaluation, ensure you've set `personal_api_key`: + +Ruby + +PostHog AI + +```ruby +config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key] +``` + +See our [Ruby SDK docs](/docs/libraries/ruby.md#local-evaluation) for details on local evaluation with Puma and Unicorn servers. + +> **Note:** `PostHog.is_feature_enabled`, `PostHog.get_feature_flag`, `PostHog.get_feature_flag_result`, `PostHog.get_feature_flag_payload`, and `PostHog.capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `PostHog.evaluate_flags` for new code. + +## Testing + +In your test environment, disable network calls with test mode: + +config/environments/test.rb + +PostHog AI + +```ruby +PostHog.init do |config| + config.api_key = '' + config.test_mode = true +end +``` + +Or in your specs: + +spec/rails\_helper.rb + +PostHog AI + +```ruby +RSpec.configure do |config| + config.before(:each) do + allow(PostHog).to receive(:capture) + end +end +``` + +## Configuration reference + +### Core PostHog options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| api_key | String | required | Your PostHog project token. | +| host | String | https://us.i.posthog.com | Fully qualified PostHog API host. | +| personal_api_key | String | nil | Personal API key for local feature flag evaluation and remote config payloads. | +| max_queue_size | Integer | 10000 | Maximum number of events to keep in the async queue before dropping new events. | +| test_mode | Boolean | false | Keep events queued and do not send them. Useful for tests. | +| sync_mode | Boolean | false | Send events synchronously on the calling thread. | +| on_error | Proc | no-op | Callback called as on_error.call(status, error). | +| feature_flags_polling_interval | Integer | 30 | Seconds between local feature flag definition polls. | +| feature_flag_request_timeout_seconds | Integer | 3 | Timeout, in seconds, for feature flag requests. | +| before_send | Proc | nil | Callback that receives the event hash before it is queued or sent. Return a modified event hash, or nil to drop the event. | + +The `PostHog.init` block supports the options above. Less common core options like `batch_size`, `disable_singleton_warning`, `skip_ssl_verification`, and `flag_definition_cache_provider` can be passed as an options hash to `PostHog.init(...)`; see the [Ruby SDK docs](/docs/libraries/ruby.md#configuration) for details. + +### Rails-specific options + +Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| auto_capture_exceptions | Boolean | false | Automatically capture exceptions. | +| report_rescued_exceptions | Boolean | false | Report exceptions Rails rescues. | +| auto_instrument_active_job | Boolean | false | Capture ActiveJob exceptions with job context. | +| excluded_exceptions | Array | [] | Additional exception class names to ignore. | +| use_tracing_headers | Boolean | true | Use X-PostHog-Distinct-Id and X-PostHog-Session-Id as request-scoped defaults. | +| capture_user_context | Boolean | true | Include authenticated user info in exceptions. | +| current_user_method | Symbol | :current_user | Controller method used to fetch the current user. | +| user_id_method | Symbol | nil | Method used to extract the distinct ID from the user object. Auto-detects when nil. | + +## Troubleshooting + +### Exceptions not being captured + +1. Verify PostHog is initialized: + + Ruby + + PostHog AI + + ```ruby + Rails.console + > PostHog.initialized? + => true + ``` + +2. Check your excluded exceptions list. + +3. Verify middleware is installed: + + Ruby + + PostHog AI + + ```ruby + Rails.application.middleware + ``` + +### User context not working + +1. Verify `current_user_method` matches your controller method. +2. Check that the user object responds to `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid`. +3. If using a custom identifier, set `PostHog::Rails.config.user_id_method = :your_method`. + +### Feature flags not working + +Ensure you've set `personal_api_key` in your configuration. + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Rails (such as analytics, feature flags, A/B testing, etc.), have a look at our [Ruby SDK docs](/docs/libraries/ruby.md). + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby.md b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby.md new file mode 100644 index 00000000..bdacec9f --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby-on-rails/references/ruby.md @@ -0,0 +1,206 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby Feature Flags installation - Docs + +Copy page + +# Ruby Feature Flags installation - Docs + +1. 1 + + ## Install the gem + + Required + + Add the PostHog Ruby gem to your Gemfile: + + Gemfile + + PostHog AI + + ```ruby + gem "posthog-ruby" + ``` + +2. 2 + + ## Configure PostHog + + Required + + Initialize the PostHog client with your project token and host: + + Ruby + + PostHog AI + + ```ruby + require 'posthog' + posthog = PostHog::Client.new({ + api_key: "", + host: "https://us.i.posthog.com", + on_error: Proc.new { |status, msg| print msg } + }) + ``` + +3. 3 + + ## Send events + + Recommended + + Once installed, you can manually send events to test your integration: + + Ruby + + PostHog AI + + ```ruby + posthog.capture({ + distinct_id: 'user_123', + event: 'button_clicked', + properties: { + button_name: 'signup' + } + }) + ``` + +4. 4 + + ## Evaluate boolean feature flags + + Required + + Check if a feature flag is enabled: + + ```ruby + is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') + if is_my_flag_enabled + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + end + ``` + +5. 5 + + ## Evaluate multivariate feature flags + + Optional + + For multivariate flags, check which variant the user has been assigned: + + ```ruby + enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') + if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + end + ``` + +6. 6 + + ## Include feature flag information in events + + Required + + If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + + **Note:** This step is only required for events captured using our server-side SDKs or API. + + ## Set send_feature_flags (recommended) + + Set `send_feature_flags` to `true` in your capture call: + + Ruby + + PostHog AI + + ```ruby + posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + send_feature_flags: true, + }) + ``` + + ## Include $feature property + + Include the `$feature/feature_flag_name` property in your event properties: + + Ruby + + PostHog AI + + ```ruby + posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } + }) + ``` + +7. 7 + + ## Override server properties + + Optional + + Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with: + + ```ruby + posthog.get_feature_flag( + 'flag-key', + 'distinct_id_of_the_user', + person_properties: { + 'property_name': 'value' + }, + groups: { + 'your_group_type': 'your_group_id', + 'another_group_type': 'your_group_id', + }, + group_properties: { + 'your_group_type': { + 'group_property_name': 'value' + }, + 'another_group_type': { + 'group_property_name': 'value' + }, + }, + ) + ``` + +8. 8 + + ## Running experiments + + Optional + + Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard. + +9. 9 + + ## Next steps + + Recommended + + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. + + | Resource | Description | + | --- | --- | + | [Creating a feature flag](/docs/feature-flags/creating-feature-flags.md) | How to create a feature flag in PostHog | + | [Adding feature flag code](/docs/feature-flags/adding-feature-flag-code.md) | How to check flags in your code for all platforms | + | [Framework-specific guides](/docs/feature-flags/tutorials.md#framework-guides) | Setup guides for React Native, Next.js, Flutter, and other frameworks | + | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | + | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-ruby/SKILL.md b/skills/posthog/all/skills/feature-flags-ruby/SKILL.md index fc7f80dd..10f5b178 100644 --- a/skills/posthog/all/skills/feature-flags-ruby/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-ruby/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-ruby description: PostHog feature flags for Ruby applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Ruby @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Ruby applications. - `references/ruby.md` - Ruby feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,6 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) - Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs - In CLIs and scripts: MUST call client.shutdown before exit or all events are lost diff --git a/skills/posthog/all/skills/feature-flags-ruby/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-ruby/references/COMMANDMENTS.md new file mode 100644 index 00000000..6652b15b --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-ruby/references/COMMANDMENTS.md @@ -0,0 +1,11 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-ruby is the Ruby SDK gem name (add `gem 'posthog-ruby'` to Gemfile) but require it with `require 'posthog'` (NOT `require 'posthog-ruby'`) +- Use PostHog::Client.new(api_key: key, host: host) for instance-based initialization in scripts and CLIs +- In CLIs and scripts: MUST call client.shutdown before exit or all events are lost +- Use begin/rescue/ensure with shutdown in the ensure block for proper cleanup +- capture and identify take a single hash argument: client.capture(distinct_id: 'user_123', event: 'my_event', properties: { key: 'value' }) +- capture_exception takes POSITIONAL args (not keyword): client.capture_exception(exception, distinct_id, additional_properties) — do NOT use `distinct_id:` keyword syntax diff --git a/skills/posthog/all/skills/feature-flags-ruby/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-ruby/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-ruby/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-ruby/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ruby/references/best-practices.md b/skills/posthog/all/skills/feature-flags-ruby/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-ruby/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-ruby/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-ruby/references/ruby.md b/skills/posthog/all/skills/feature-flags-ruby/references/ruby.md index 282b610b..bdacec9f 100644 --- a/skills/posthog/all/skills/feature-flags-ruby/references/ruby.md +++ b/skills/posthog/all/skills/feature-flags-ruby/references/ruby.md @@ -1,4 +1,10 @@ -# Ruby feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Ruby Feature Flags installation - Docs + +Copy page + +# Ruby Feature Flags installation - Docs 1. 1 @@ -191,9 +197,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-rust/SKILL.md b/skills/posthog/all/skills/feature-flags-rust/SKILL.md index 1b6e2be4..b13585c1 100644 --- a/skills/posthog/all/skills/feature-flags-rust/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-rust/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-rust description: PostHog feature flags for Rust applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Rust @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Rust applications. - `references/rust.md` - Rust feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,13 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/feature-flags-rust/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-rust/references/COMMANDMENTS.md new file mode 100644 index 00000000..4eb31257 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-rust/references/COMMANDMENTS.md @@ -0,0 +1,14 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- posthog-rs is the Rust SDK crate; add it with `cargo add posthog-rs` and construct the client with `posthog_rs::client(options).await` +- Create one client per process and share it (for example an `Arc` in your app state or a `OnceCell`); do not build a new client per request or task +- Configure the project token and host from environment variables via `ClientOptions` (the `(api_key, host)` tuple); never hardcode PostHog secrets +- Because `capture` is fire-and-forget, call `client.flush().await` then `client.shutdown().await` before the process exits — where the server future resolves. An app with no shutdown path still needs this; add the calls there rather than skipping them so queued events are delivered before exit +- Server-side captures must set a stable `distinct_id` on `Event::new(event, distinct_id)` that matches frontend identify calls; avoid anonymous or literal IDs for business events +- The SDK has no `identify` or `alias` helper; set person properties by inserting a `$set` property on an event +- For feature flags, call `client.evaluate_flags(distinct_id, EvaluateFlagsOptions::default()).await` once per user/request, then read values from the returned snapshot with `is_enabled(...)` +- For error tracking, use `client.capture_exception_with(&err, CaptureExceptionOptions::new()...)`; the `error-tracking` feature is enabled by default in recent versions +- The Rust SDK has no surveys or session replay support; do not promise or scaffold those features diff --git a/skills/posthog/all/skills/feature-flags-rust/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-rust/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-rust/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-rust/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-rust/references/best-practices.md b/skills/posthog/all/skills/feature-flags-rust/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-rust/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-rust/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-rust/references/rust.md b/skills/posthog/all/skills/feature-flags-rust/references/rust.md index 9cc1afd3..240c7c76 100644 --- a/skills/posthog/all/skills/feature-flags-rust/references/rust.md +++ b/skills/posthog/all/skills/feature-flags-rust/references/rust.md @@ -1,4 +1,10 @@ -# Rust feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Rust Feature Flags installation - Docs + +Copy page + +# Rust Feature Flags installation - Docs Install the `posthog-rs` crate by adding it to your `Cargo.toml`. @@ -8,7 +14,7 @@ PostHog AI ```toml [dependencies] -posthog-rs = "0.3.5" +posthog-rs = "0.14" ``` Next, set up the client with your PostHog project key. @@ -18,7 +24,7 @@ Rust PostHog AI ```rust -let client = posthog_rs::client(env!("")); +let client = posthog_rs::client("").await; ``` ### Blocking client @@ -33,138 +39,176 @@ PostHog AI ```toml [dependencies] -posthog-rs = { version = "0.3.5", default-features = false } +posthog-rs = { version = "0.14", default-features = false } ``` -In blocking mode, calls to `capture` and related methods will block until the PostHog event capture API returns – generally this is on the order of tens of milliseconds, but you may want to `thread::spawn` a background thread when you send an event. +With the blocking client, the same methods are available without `.await`. Either way, `capture` is non-blocking: it hands the event to a background worker that batches and sends it, so it returns immediately instead of waiting on the network. Because delivery happens in the background, call `flush()` or `shutdown()` before your program exits, or buffered events may be lost. ## Using feature flags -### Boolean feature flags +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Fetching all flags +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); +if flags.is_enabled("flag-key") { + // Do something differently for this user } +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); ``` -### Feature flag payloads +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() -).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### With person properties +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, - Some(person_props), - None -).await.unwrap(); +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### With groups +### Evaluating only specific flags -For B2B applications with group-based flags: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, - Some(group_props) +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. | Resource | Description | @@ -175,9 +219,9 @@ Now that you're evaluating flags, continue with the resources below to learn wha | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-web/SKILL.md b/skills/posthog/all/skills/feature-flags-web/SKILL.md index 801edad6..9680a671 100644 --- a/skills/posthog/all/skills/feature-flags-web/SKILL.md +++ b/skills/posthog/all/skills/feature-flags-web/SKILL.md @@ -3,7 +3,7 @@ name: feature-flags-web description: PostHog feature flags for Web (JavaScript) applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog feature flags for Web (JavaScript) @@ -14,7 +14,8 @@ This skill helps you add PostHog feature flags to Web (JavaScript) applications. - `references/web.md` - Web feature flags installation - docs - `references/adding-feature-flag-code.md` - Adding feature flag code - docs -- `references/best-practices.md` - Feature flag best practices - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow Consult the documentation for API details and framework-specific patterns. @@ -31,4 +32,7 @@ Check if a PostHog MCP server is connected. If available, look for tools related ## Framework guidelines -_No specific framework guidelines._ +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-web/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-web/references/COMMANDMENTS.md new file mode 100644 index 00000000..64d91138 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-web/references/COMMANDMENTS.md @@ -0,0 +1,8 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it diff --git a/skills/posthog/all/skills/feature-flags-web/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-web/references/adding-feature-flag-code.md index 4c56a266..79f461a6 100644 --- a/skills/posthog/all/skills/feature-flags-web/references/adding-feature-flag-code.md +++ b/skills/posthog/all/skills/feature-flags-web/references/adding-feature-flag-code.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + # Adding feature flag code - Docs Once you've created your feature flag in PostHog, the next step is to add your code: @@ -11,10 +17,11 @@ Web PostHog AI ```javascript -if (posthog.isFeatureEnabled('flag-key') ) { +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload } ``` @@ -25,10 +32,25 @@ Web PostHog AI ```javascript -if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) } ``` @@ -65,6 +87,24 @@ The `onFeatureFlags` callback receives the following parameters: You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + ### Reloading feature flags Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: @@ -159,7 +199,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). }) ``` @@ -213,7 +253,7 @@ PostHog provides several hooks to make it easy to use feature flags in your Reac | Hook | Description | | --- | --- | -| useFeatureFlagEnabled | Returns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event. | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | | useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | | useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | | useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | @@ -225,7 +265,7 @@ React PostHog AI ```jsx -import { useFeatureFlagEnabled } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const showWelcomeMessage = useFeatureFlagEnabled('flag-key') const payload = useFeatureFlagPayload('flag-key') @@ -250,6 +290,16 @@ function App() { export default App; ``` +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + #### Example 2: Using a multivariate feature flag React @@ -298,7 +348,7 @@ React PostHog AI ```jsx -import { useFeatureFlagPayload } from '@posthog/react' +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' function App() { const variant = useFeatureFlagEnabled('show-welcome-message') const payload = useFeatureFlagPayload('show-welcome-message') @@ -391,7 +441,7 @@ PostHog AI ```javascript posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30', feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). } ) @@ -435,9 +485,11 @@ try { ## Node.js -There are 2 steps to implement feature flags in Node: +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -446,11 +498,11 @@ Node.js PostHog AI ```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if (isFeatureFlagEnabled) { - // Your code if the flag is enabled +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` @@ -461,14 +513,19 @@ Node.js PostHog AI ```javascript -const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant) + const matchedFlagPayload = flags.getFlagPayload('flag-key') } ``` +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -477,47 +534,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Node.js PostHog AI ```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + flags, }) ``` -#### Method 2: Set `sendFeatureFlags` to `true` - -The `capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `sendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Node.js PostHog AI ```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: true, + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -#### Advanced usage (v5.5.0+) +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 5.5.0, `sendFeatureFlags` can also accept an options object for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Node.js @@ -527,66 +591,34 @@ PostHog AI client.capture({ distinctId: 'distinct_id_of_your_user', event: 'event_name', - sendFeatureFlags: { - onlyEvaluateLocally: true, - personProperties: { plan: 'premium' }, - groupProperties: { org: { tier: 'enterprise' } } - } + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `sendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v5.5.0 - -Prior to version 5.5.0, feature flags were automatically sent with events when using local evaluation, even when `sendFeatureFlags` was not explicitly set. This behavior has been **removed** in v5.5.0 to be more predictable and explicit. +### Evaluating only specific flags -If you were relying on this automatic behavior, you must now explicitly set `sendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `getAllFlags()` or `getAllFlagsAndPayloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: Node.js PostHog AI ```javascript -await client.getAllFlags('distinct_id_of_your_user') -await client.getAllFlagsAndPayloads('distinct_id_of_your_user') +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. -1. You call `posthog.getFeatureFlag()` or `posthog.isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: - -Node.js - -PostHog AI - -```javascript -const isFeatureFlagEnabled = await client.isFeatureEnabled( - 'flag-key', - 'distinct_id_of_your_user', - { - 'sendFeatureFlagEvents': false - }) -``` +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. ### Advanced: Overriding server properties @@ -601,27 +633,26 @@ Node.js PostHog AI ```javascript -await client.getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - { - personProperties: { - 'property_name': 'value' - }, - groups: { - "your_group_type": "your_group_id", - "another_group_type": "your_group_id", +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', }, - groupProperties: { - 'your_group_type': { - 'group_property_name': 'value' - }, - 'another_group_type': { - 'group_property_name': 'value' - } + another_group_type: { + group_property_name: 'value', }, - } -) + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -653,7 +684,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. JavaScript @@ -661,53 +692,18 @@ PostHog AI ```javascript const client = new PostHog('', { - api_host: 'https://us.i.posthog.com', - feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). - } -) -``` - -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -JavaScript - -PostHog AI - -```javascript -async function handleFeatureFlag(client, flagKey, distinctId) { - try { - const isEnabled = await client.isFeatureEnabled(flagKey, distinctId); - console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); - return isEnabled; - } catch (error) { - console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw error; - } -} -// Usage example -try { - const flagEnabled = await handleFeatureFlag(client, 'new-feature', 'user-123'); - if (flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (error) { - // Handle the error at a higher level - console.error('Feature flag check failed, using default behavior'); - // Implement fallback logic -} + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) ``` ## Python -There are 2 steps to implement feature flags in Python: +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -716,11 +712,11 @@ Python PostHog AI ```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled: +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` #### Multivariate feature flags @@ -730,13 +726,18 @@ Python PostHog AI ```python -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') -if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload("flag-key") ``` +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -745,47 +746,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Python PostHog AI ```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass posthog.capture( "event_name", - distinct_id="distinct_id_of_the_user", - properties={ - "$feature/feature-flag-key": "variant-key" # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - }, + distinct_id="distinct_id_of_your_user", + flags=flags, ) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -#### Basic usage - -Setting `send_feature_flags` to `True` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Python PostHog AI ```python +# Attach only flags accessed with is_enabled() or get_flag() before this call posthog.capture( - distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags=True + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), ) ``` -## Advanced usage (v6.3.0+) +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually -As of version 6.3.0, `send_feature_flags` can also accept a dictionary for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Python @@ -793,64 +801,37 @@ PostHog AI ```python posthog.capture( + "event_name", distinct_id="distinct_id_of_the_user", - event='event_name', - send_feature_flags={ - 'only_evaluate_locally': True, - 'person_properties': {'plan': 'premium'}, - 'group_properties': {'org': {'tier': 'enterprise'}} - } + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, ) ``` -#### Performance considerations +### Evaluating only specific flags -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: True` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v6.3.0 - -Prior to version 6.3.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v6.3.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags=True` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Python PostHog AI ```python -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. -Python +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -PostHog AI - -```python -is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user', send_feature_flag_events=False) -# will not send `$feature_flag_called` events -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Advanced: Overriding server properties @@ -865,18 +846,20 @@ Python PostHog AI ```python -posthog.get_feature_flag( - 'flag-key', - 'distinct_id_of_the_user', - person_properties={'property_name': 'value'}, +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, groups={ - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id'}, + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, group_properties={ - 'your_group_type': {'group_property_name': 'value'}, - 'another_group_type': {'group_property_name': 'value'} + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, }, ) +if flags.is_enabled("flag-key"): + # Do something differently for this user ``` ### Overriding GeoIP properties @@ -908,52 +891,27 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Python PostHog AI ```python -posthog = Posthog('', - host='https://us.i.posthog.com' - feature_flags_request_timeout_seconds=3 // Time in second. Default is 3 +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Python - -PostHog AI - -```python -def handle_feature_flag(client, flag_key, distinct_id): - try: - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - print(f"Feature flag '{flag_key}' for user '{distinct_id}' is {'enabled' if is_enabled else 'disabled'}") - return is_enabled - except Exception as e: - print(f"Error fetching feature flag '{flag_key}': {str(e)}") - raise e -# Usage example -try: - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled: - # Implement new feature logic - else: - # Implement old feature logic -except Exception as e: - # Handle the error at a higher level -``` - ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -962,9 +920,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` @@ -975,12 +935,21 @@ PHP PostHog AI ```php -$enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') -if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant - # Do something differently for this user +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); } ``` +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -989,81 +958,113 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. PHP PostHog AI ```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. - -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags PostHog::capture([ - 'distinctId' => 'distinct_id_of_your_user', - 'event' => 'event_name', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); ``` -### Sending `$feature_flag_called` events +### Evaluating only specific flags -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. +PHP -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +PostHog AI -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1077,21 +1078,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ 'your_group_type' => ['group_property_name' => 'value'], - 'another_group_type' => ['group_property_name' => 'value'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1123,7 +1124,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. PHP @@ -1131,54 +1132,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] ); ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1187,11 +1154,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1202,14 +1169,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant # Do something differently for this user # Optional: fetch the payload - matched_flag_payload = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1218,47 +1190,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Ruby PostHog AI ```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: true, + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1268,65 +1247,66 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. +### Evaluating only specific flags -#### Breaking change in v3.1.0 +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. +Ruby -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. +PostHog AI -### Fetching all flags for a user +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating locally only -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: Ruby PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +### Disabling GeoIP for flag evaluation -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_your_user', - send_feature_flag_events: true) + disable_geoip: true, +) ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -1340,25 +1320,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', }, group_properties: { - 'your_group_type': { - 'group_property_name': 'value' - } - 'another_group_type': { - 'group_property_name': 'value' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1390,7 +1372,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Ruby @@ -1398,52 +1380,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1452,15 +1400,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1471,18 +1420,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ DistinctId: "distinct_id_of_your_user", }) if err != nil { // Handle error (e.g. capture error and fallback to default behavior) } -if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1491,46 +1446,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Go PostHog AI ```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - Properties: posthog.NewProperties(). - Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage - -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: true, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1538,77 +1506,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, }) ``` ### Sending `$feature_flag_called` events -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` - -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1623,25 +1549,26 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag( - FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_the_user", - Groups: posthog.NewGroups(). - Set("your_group_type", "your_group_id"). - Set("another_group_type", "your_group_id"), - PersonProperties: posthog.NewProperties(). - Set("property_name", "value"), - GroupProperties: map[string]map[string]interface{}{ - "your_group_type": { - "group_property_name": "value", - }, - "another_group_type": { - "group_property_name": "value", - }, - }, +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), }, -) +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1673,47 +1600,24 @@ Simply include any of these properties in the `person_properties` parameter alon ### Request timeout -You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. Go PostHog AI ```go +// import "time" client, _ := posthog.NewWithConfig( - os.Getenv(""), - posthog.Config{ - PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. - Endpoint: "https://us.i.posthog.com", - FeatureFlagRequestTimeout: 3 // Time in seconds. Default is 3. - }, + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, ) ``` -### Error handling - -When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -1776,8 +1680,22 @@ posthog.isFeatureEnabled('key-for-your-boolean-flag') posthog.getFeatureFlag('key-for-your-boolean-flag') // Multivariant feature flags are returned as a string posthog.getFeatureFlag('key-for-your-multivariate-flag') -// Optional fetch the payload returns 'JsonType' or undefined if not loaded yet or if there was a problem loading -posthog.getFeatureFlagPayload('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} ``` ### Ensuring flags are loaded before usage @@ -1985,10 +1903,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -2000,10 +1919,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -2031,7 +1966,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -2052,33 +1987,79 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` -## iOS +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads -### Boolean feature flags +If your payload is a JSON object, you can decode it into a `Decodable` type: Swift PostHog AI ```swift -if (PostHogSDK.shared.isFeatureEnabled("flag-key")) { - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title } ``` -### Multivariate feature flags +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: Swift PostHog AI ```swift -if (PostHogSDK.shared.getFeatureFlag("flag-key") as? String == "variant-key") { // replace "variant-key" with the key of your variant - // Do something differently for this user - // Optional: fetch the payload - let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagPayload("flag-key") +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) } ``` @@ -2116,10 +2097,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { name: PostHogSDK.didReceiveFeatureFlags, object: nil ) - let POSTHOG_API_KEY = "" + let POSTHOG_PROJECT_TOKEN = "" // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' let POSTHOG_HOST = "https://us.i.posthog.com" - let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true } @@ -2145,6 +2126,19 @@ PostHogSDK.shared.reloadFeatureFlags { } ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + ## Flutter ### Boolean feature flags @@ -2154,10 +2148,11 @@ Dart PostHog AI ```dart -if (await Posthog().isFeatureEnabled('flag-key')) { +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` @@ -2168,16 +2163,17 @@ Dart PostHog AI ```dart -if (await Posthog().getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - final matchedFlagPayload = await Posthog().getFeatureFlagPayload('flag-key'); + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; } ``` ### Ensuring flags are loaded before usage -> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation) by disabling the `com.posthog.posthog.AUTO_INIT` mode. +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. @@ -2214,34 +2210,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags Java PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user } +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2255,19 +2368,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + PostHogEvaluateFlagsOptions.builder() .group("your_group_type", "your_group_id") .group("another_group_type", "your_group_id") .groupProperty("your_group_type", "group_property_name", "value") .groupProperty("another_group_type", "group_property_name", "value") .personProperty("property_name", "value") - .build()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2299,9 +2413,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2310,15 +2426,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +if flags.is_enabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } ``` @@ -2329,29 +2445,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2361,236 +2472,245 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} let mut event = Event::new("event_name", "distinct_id_of_your_user"); -event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); -client.capture(event).unwrap(); +event.with_flags(&flags); +client.capture(event); ``` -#### Method 2: Fetch and include all flags +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap(); +// Attach only flags accessed with is_enabled() or get_flag() before this call let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; -} -client.capture(event).unwrap(); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); ``` -### Fetching all flags for a user +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `get_feature_flags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); ``` -### Feature flag payloads +### Evaluating only specific flags -You can retrieve additional data associated with a feature flag using payloads: +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, ).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} ``` -### With person properties +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. -You can include person properties for more targeted flag evaluation: +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: Rust PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties -).await.unwrap(); +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} ``` -### With groups (B2B) +## Elixir + +There are two steps to implement feature flags in Elixir: -For B2B applications with group-based flags: +### Step 1: Evaluate flags once -Rust +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir PostHog AI -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Blocking client - -If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: +#### Multivariate feature flags -Rust +Elixir PostHog AI -```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).unwrap(); +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end ``` -### Error handling +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. -Rust +### Step 2: Include feature flag information when capturing events -PostHog AI +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } -} -``` +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). -## Elixir +There are two methods you can use to include feature flag information in your events: -`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag in Elixir. More documentation on it can be found in the [HexPM Docs](https://hexdocs.pm/posthog/PostHog.FeatureFlags.html). +#### Method 1: Put the evaluated flags snapshot in context -#### Boolean feature flags +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) ``` -It will attempt to take `distinct_id` from the context if it's not provided. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: Elixir PostHog AI ```elixir -iex> PostHog.set_context(%{distinct_id: "user123"}) -:ok -iex> PostHog.FeatureFlags.check("example-feature-flag-1") -{:ok, true} +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) ``` -#### Multivariate feature flags +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123") -{:ok, "variant2"} +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) ``` -### Errors +### Evaluating only specific flags -We'll return an error if the feature flag doesn't exist. +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: Elixir PostHog AI ```elixir -iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123") -{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}} +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) ``` -You can also use `PostHog.FeatureFlags.check!/2` if you're feeling adventurous or running a script and prefer errors to be raised instead. +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2599,15 +2719,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2618,33 +2735,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ // Do something differently for this user // Optional: fetch the payload - var matchedPayload = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. - -C# - -PostHog AI +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2654,91 +2757,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. C# PostHog AI ```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} posthog.Capture( "distinct_id_of_your_user", "event_name", - properties: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + flags: flags.Only("checkout-flow", "new-dashboard") ); ``` -### Fetching all flags for a user +#### Method 2: Include the `$feature/feature_flag_name` property manually -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. - -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + ### Advanced: Overriding server properties Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. @@ -2752,50 +2866,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -3330,7 +3425,7 @@ headers = { payload = { "api_key": "", "event": "your_event_name", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant } @@ -3376,7 +3471,7 @@ headers = { payload = { "api_key": "", "event": "feature_flag_called", - "distinct_id": "distinct_id_of_your_user, + "distinct_id": "distinct_id_of_your_user", "properties": { "$feature_flag": "feature-flag-key", "$feature_flag_response": "variant-name" @@ -3480,9 +3575,9 @@ The list of properties that this overrides: 6. `$geoip_postal_code` 7. `$geoip_time_zone` -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-web/references/best-practices.md b/skills/posthog/all/skills/feature-flags-web/references/best-practices.md index 1bb4addd..7831a883 100644 --- a/skills/posthog/all/skills/feature-flags-web/references/best-practices.md +++ b/skills/posthog/all/skills/feature-flags-web/references/best-practices.md @@ -1,138 +1,236 @@ -# Feature flag best practices - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt -## 1\. Use a reverse proxy +# Best practices for production-ready flags - Docs -Ad blockers have the potential to disable your feature flags, which can lead to bad experiences, such as users seeing the wrong version of your app, or missing a new feature rollout. +Copy page -To avoid this, deploy a reverse proxy, which enables you to make requests and send events to PostHog Cloud using your own domain. +# Best practices for production-ready flags - Docs -This means that requests are less likely to be intercepted by tracking blockers, and your feature flags are more likely to work as intended. You'll also capture more usage data. +## Checklist -PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. See our [reverse proxy docs](/docs/advanced/proxy.md) for more. +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. -## 2\. Call your flag in as few places as possible +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. -It should be easy to understand how feature flags affect your code. The more locations a flag is in, the more likely it is to cause problems. For example, a developer could remove the flag in one place but forget to remove it in another. +--- -If you expect to use a feature flag in multiple places, it's a good idea to wrap the flag in a single function or method. For example: +## Flags are pure functions -JavaScript +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. PostHog AI -```javascript -function useBetaFeature() { - return posthog.isFeatureEnabled('beta-feature') -} ``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. -## 3\. Targeting +## Unexpected results are almost always input problems -PostHog evaluates flags based on the user's distinct ID, having different IDs can cause the same user to receive different flag values across different sessions, devices, and platforms. By [identifying](/docs/getting-started/identify-users.md) them, you can ensure consistent flag values. +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. -The same applies to identifying [groups](/docs/product-analytics/group-analytics.md) for group-level flags. +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. -For flags targeting anonymous users, such as signup flows or landing page experiments, consider using [device bucketing](/docs/feature-flags/device-bucketing.md) instead. This evaluates the flag based on the device ID, ensuring a consistent experience on the device even after the user logs in. +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. -## 4\. Use server-side local evaluation for faster flags +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** -Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. +When something goes wrong, in order of likelihood: -Evaluate flags locally when possible, since this enables you to resolve flags faster and with fewer API calls. See our docs on [local evaluation](/docs/feature-flags/local-evaluation.md) for more details. +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. -## 5\. Bootstrap flags on the client to make them available immediately +## Resolve identity before evaluating flags -Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. -To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. -See our docs on [bootstrapping](/docs/feature-flags/bootstrapping.md) for more details on how to do this. +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. -## 6\. Naming tips +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. -Good naming conventions for your flags makes them easier to understand and maintain. Below are tips for naming your flags: +### Don't rely on flag persistence to fix identity gaps -- **Use descriptive names.** For example, `is_v2_billing_dashboard_enabled` is much clearer than `is_dashboard_enabled`. +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. -- **Use name "types".** This helps organize them and makes their purpose clear. Types might include experiments, releases, and permissions. For example, instead of `new-billing`, they would be `new-billing-experiment` or `new-billing-release`. +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. -- **Name flags to reflect their return type.** For example, `is_premium_user` for a boolean, `enabled_integrations` for an array, or `selected_theme` for a single string. +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. -- **Use positive language for boolean flags.** For example, `is_premium_user` instead of `is_not_premium_user`. This helps avoid double negatives when checking the flag value (e.g. `if !is_not_premium_user` is confusing). +## Evaluation architecture -## 7\. Roll out progressively +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. -When testing a change behind a feature flag, it is best to roll it out to a small group of users and increase that group over time. This is also known as a [phased rollout](/tutorials/phased-rollout.md). It enables you to identify any potential issues ahead of the full release. +### Evaluate once, not continuously -For example, at PostHog we often roll out the flag to just the responsible developer. It then moves on to the internal team, then beta users, and finally into a full rollout. This enables us to [test in production](/product-engineers/testing-in-production.md), get multiple rounds of feedback, identify issues, and polish the feature before the full release. +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. -## 8\. Clean up after yourself +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. -Leaving flags in your code for too long can confuse future developers and create technical debt, especially if it's already rolled out and integrated. Be sure to remove stale flags once they are completely rolled out or no longer needed. +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. -When you have many flags to clean up, use [bulk delete](/docs/feature-flags/creating-feature-flags.md#deleting-feature-flags-in-bulk) to select and delete multiple flags at once. Select flags using checkboxes (shift-click to select a range), or filter by name or status and use "select all matching" to select all flags that match your criteria. PostHog validates that flags aren't used by experiments, early access features, or other dependent flags before deletion. +### Evaluate where the data lives -## 9\. Fallback to working code +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. -It's possible that a feature flag will return an [unexpected value](/docs/feature-flags/common-questions.md#my-feature-flag-called-events-show-none-empty-string-or-false-instead-of-my-variant-names). For example, if the flag is disabled or failed to load due to a network error. +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. -In this case, its best to check that the feature flag returns a valid expected value before using it. If it isn't, fallback to working code. +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. -## 10\. Use dependencies for complex rollouts +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. -For sophisticated feature rollouts, consider using [feature flag dependencies](/docs/feature-flags/dependencies.md) where one flag's activation depends on another flag's state. This is useful for: +### Server-side local evaluation is the recommended default -- Enabling complex features only after foundational components are active -- Running experiments only on users with specific features enabled -- Creating safety mechanisms where critical flags must be enabled first +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: -When using dependencies, keep the dependency chains simple and avoid circular dependencies. +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. -## 11\. Reducing your bill +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. -We aim to be significantly cheaper than our competitors. To help you reduce your bill, we've created a [dedicated guide](/docs/feature-flags/cutting-costs.md) to estimating and reducing your feature flag costs. +### Have the value before you need it -## 12\. Consistent flag evaluations across frontend and backend +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. -For feature flags with flag persistence enabled and used across both your frontend and backend, you will need to do one of the following to ensure the evaluation result of the flag is consistent between both environments: +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. -1. Identify the user on the frontend and use the same identified distinct ID when evaluating the flag on the backend. +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." JavaScript PostHog AI ```javascript -// Frontend: Identify the user -posthog.identify('user123') -// Backend: Use the same distinct ID -const flagValue = await posthog.getFeatureFlag('my-flag', 'user123') +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} ``` -2. If you are unable to call identify on the frontend, and only have access to the anonymous distinct ID when evaluating the flag on the backend, you can include the anonymous distinct ID as a person property override in the `getFeatureFlag` call. +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: JavaScript PostHog AI ```javascript -// Frontend: Get the anonymous ID (before identify is called) -const anonId = posthog.getAnonymousId() -// Backend: Pass the anonymous ID as a person property override -const flagValue = await posthog.getFeatureFlag( - 'my-flag', - 'user123', // identified distinct ID - { - personProperties: { - $anon_distinct_id: anonId - } - } -) +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} ``` -### Community questions +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-web/references/web.md b/skills/posthog/all/skills/feature-flags-web/references/web.md index 0e009da2..a8fc3ce9 100644 --- a/skills/posthog/all/skills/feature-flags-web/references/web.md +++ b/skills/posthog/all/skills/feature-flags-web/references/web.md @@ -1,4 +1,10 @@ -# Web feature flags installation - Docs +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Web Feature Flags installation - Docs + +Copy page + +# Web Feature Flags installation - Docs 1. 1 @@ -18,10 +24,10 @@ ```html ``` @@ -58,7 +64,7 @@ import posthog from 'posthog-js' posthog.init('', { api_host: 'https://us.i.posthog.com', - defaults: '2026-01-30' + defaults: '2026-05-30' }) ``` @@ -94,7 +100,7 @@ if (posthog.isFeatureEnabled('flag-key')) { // Do something differently for this user // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload } ``` @@ -107,10 +113,11 @@ For multivariate flags, check which variant the user has been assigned: ```javascript - if (posthog.getFeatureFlag('flag-key') == 'variant-key') { // replace 'variant-key' with the key of your variant + const matchedFlag = posthog.getFeatureFlagResult('flag-key') + if (matchedFlag?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant // Do something differently for this user - // Optional: fetch the payload - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + // Optional: read the payload from the same result + const matchedFlagPayload = matchedFlag?.payload } ``` @@ -123,7 +130,7 @@ Feature flags can include payloads with additional data. Fetch the payload like this: ```javascript - const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key') + const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload ``` 6. 6 @@ -183,9 +190,9 @@ | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/feature-flags-wordpress/SKILL.md b/skills/posthog/all/skills/feature-flags-wordpress/SKILL.md new file mode 100644 index 00000000..c80617bd --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/SKILL.md @@ -0,0 +1,50 @@ +--- +name: feature-flags-wordpress +description: PostHog feature flags for WordPress applications +metadata: + author: PostHog + version: dev +--- + +# PostHog feature flags for WordPress + +This skill helps you add PostHog feature flags to WordPress applications. + +## Reference files + +- `references/php.md` - Php feature flags installation - docs +- `references/wordpress.md` - How to set up wordpress analytics with PostHog - docs +- `references/adding-feature-flag-code.md` - Adding feature flag code - docs +- `references/best-practices.md` - Best practices for production-ready flags - docs +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow + +Consult the documentation for API details and framework-specific patterns. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add feature flag code alongside existing logic. Don't replace or restructure existing code. +- **Boolean flags first**: Default to boolean flag checks unless the user specifically asks for multivariate flags. +- **Server-side when possible**: Prefer server-side flag evaluation to avoid UI flicker. + +## PostHog MCP tools + +Check if a PostHog MCP server is connected. If available, look for tools related to feature flag management (creating, listing, updating, deleting flags). Use these tools to manage flags directly in PostHog rather than requiring the user to do it manually in the dashboard. + +## Framework guidelines + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Ship the integration as a standalone plugin in wp-content/plugins - do NOT edit a theme's functions.php, which is lost on theme switch or theme update +- Guard every plugin entry file with `if (!defined('ABSPATH')) { exit; }` before any other code +- Print the client snippet from a wp_head hook and escape the interpolated token with esc_js() - never echo a raw token into markup +- Read the project token from a wp-config.php constant or a WordPress option - do NOT hardcode it in plugin source +- Capture pageviews with the client SDK and reserve PostHog::capture for real server-side WordPress actions (comment_post, woocommerce_thankyou, user_register) +- Call PostHog::flush() at the end of a capture - a web request has no single exit point like a CLI script does +- Evaluate feature flags client-side on pages served by full-page caching (Varnish, batcache, WP Super Cache) - server-side flag checks are baked into the cached HTML for every visitor +- WordPress catches fatals with its own WP_Fatal_Error_Handler and renders the recovery-mode page, so a fatal can be swallowed before a PHP handler reports it - capture exceptions explicitly with PostHog::captureException in a try/catch, and do not rely on WP_DEBUG being on in production to surface them +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-wordpress/references/COMMANDMENTS.md b/skills/posthog/all/skills/feature-flags-wordpress/references/COMMANDMENTS.md new file mode 100644 index 00000000..9eb72479 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/references/COMMANDMENTS.md @@ -0,0 +1,19 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Ship the integration as a standalone plugin in wp-content/plugins - do NOT edit a theme's functions.php, which is lost on theme switch or theme update +- Guard every plugin entry file with `if (!defined('ABSPATH')) { exit; }` before any other code +- Print the client snippet from a wp_head hook and escape the interpolated token with esc_js() - never echo a raw token into markup +- Read the project token from a wp-config.php constant or a WordPress option - do NOT hardcode it in plugin source +- Capture pageviews with the client SDK and reserve PostHog::capture for real server-side WordPress actions (comment_post, woocommerce_thankyou, user_register) +- Call PostHog::flush() at the end of a capture - a web request has no single exit point like a CLI script does +- Evaluate feature flags client-side on pages served by full-page caching (Varnish, batcache, WP Super Cache) - server-side flag checks are baked into the cached HTML for every visitor +- WordPress catches fatals with its own WP_Fatal_Error_Handler and renders the recovery-mode page, so a fatal can be swallowed before a PHP handler reports it - capture exceptions explicitly with PostHog::captureException in a try/catch, and do not rely on WP_DEBUG being on in production to surface them +- Remember that source code is available in the vendor directory after composer install +- posthog/posthog-php is the PHP SDK package name +- Check composer.json for existing dependencies and autoload configuration before adding new files +- The PHP SDK uses static methods (PostHog::capture, PostHog::identify) - initialize once with PostHog::init() +- PHP SDK methods take associative arrays with 'distinctId', 'event', 'properties' keys - not positional arguments +- Any first-party loader or proxy script you add must load standalone — require its own config includes explicitly rather than assuming the app bootstrapped them diff --git a/skills/posthog/all/skills/feature-flags-wordpress/references/adding-feature-flag-code.md b/skills/posthog/all/skills/feature-flags-wordpress/references/adding-feature-flag-code.md new file mode 100644 index 00000000..79f461a6 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/references/adding-feature-flag-code.md @@ -0,0 +1,3584 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Adding feature flag code - Docs + +Copy page + +# Adding feature flag code - Docs + +Once you've created your feature flag in PostHog, the next step is to add your code: + +## Web + +### Boolean feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Multivariate feature flags + +Web + +PostHog AI + +```javascript +const result = posthog.getFeatureFlagResult('flag-key') +if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + const matchedFlagPayload = result?.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Web + +PostHog AI + +```javascript +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default). + +This means that for most pages, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Web + +PostHog AI + +```javascript +posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +#### Callback parameters + +The `onFeatureFlags` callback receives the following parameters: + +- `flags: string[]`: An object containing the feature flags that apply to the user. + +- `flagVariants: Record`: An object containing the variants that apply to the user. + +- `{ errorsLoading }: { errorsLoading?: boolean }`: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is `true` if the request timed out or if there was an error. It will be `false` or `undefined` if the request was successful. + +You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see `feature_flag_request_timeout_ms`). + +### Evaluating only specific flags + +By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass `flag_keys` when initializing PostHog: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + flag_keys: ['checkout-flow', 'new-dashboard'], +}) +``` + +PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave `flag_keys` unset to evaluate all eligible flags. + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Web + +PostHog AI + +```javascript +posthog.reloadFeatureFlags() +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +> **Note:** These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +Web + +PostHog AI + +```javascript +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +Web + +PostHog AI + +```javascript +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/manual/group-analytics.md) properties: + +Web + +PostHog AI + +```javascript +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for a given group: +posthog.resetGroupPropertiesForFlags('company') +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +#### Automatic overrides + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +#### Default overridden properties + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +This enables any geolocation-based flags to work without manually setting these properties. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +}) +``` + +### Feature flag error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## React + +There are two ways to implement feature flags in React: + +1. Using hooks. +2. Using the `` component. + +### Method 1: Using hooks + +PostHog provides several hooks to make it easy to use feature flags in your React app. + +| Hook | Description | +| --- | --- | +| useFeatureFlagEnabled | Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean \\\| undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean. | +| useFeatureFlagVariantKey | Returns the variant key of the feature flag. This sends a $feature_flag_called event. | +| useActiveFeatureFlags | Returns an array of active feature flags. This does not send a $feature_flag_called event. | +| useFeatureFlagPayload | Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey. | + +#### Example 1: Using a boolean feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const showWelcomeMessage = useFeatureFlagEnabled('flag-key') + const payload = useFeatureFlagPayload('flag-key') + return ( +
+ { + showWelcomeMessage ? ( +
+

Welcome!

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +To avoid handling `undefined` while flags are loading, pass a default value as the second argument: + +React + +PostHog AI + +```jsx +const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false) +``` + +#### Example 2: Using a multivariate feature flag + +React + +PostHog AI + +```jsx +import { useFeatureFlagVariantKey } from '@posthog/react' +function App() { + const variantKey = useFeatureFlagVariantKey('show-welcome-message') + let welcomeMessage = '' + if (variantKey === 'variant-a') { + welcomeMessage = 'Welcome to the Alpha!' + } else if (variantKey === 'variant-b') { + welcomeMessage = 'Welcome to the Beta!' + } + return ( +
+ { + welcomeMessage ? ( +
+

{welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) : ( +
+

No welcome message

+

Because the feature flag evaluated to false.

+
+ ) + } +
+ ); +} +export default App; +``` + +#### Example 3: Using a flag payload + +**Payload hook** + +The `useFeatureFlagPayload` hook does *not* send a [`$feature_flag_called`](https://posthog.com/docs/experiments/new-experimentation-engine#experiment-exposure) event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should **always** use the `useFeatureFlagPayload` hook with either the `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` hook. + +React + +PostHog AI + +```jsx +import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react' +function App() { + const variant = useFeatureFlagEnabled('show-welcome-message') + const payload = useFeatureFlagPayload('show-welcome-message') + return ( + <> + { + variant ? ( +
+

{payload?.welcomeTitle}

+

{payload?.welcomeMessage}

+
+ ) :
+

No custom welcome message

+

Because the feature flag evaluated to false.

+
+ } + + ) +} +``` + +### Method 2: Using the PostHogFeature component + +The `PostHogFeature` component simplifies code by handling feature flag related logic. + +It also automatically captures metrics, like how many times a user interacts with this feature. + +> **Note:** You still need the [`PostHogProvider`](/docs/libraries/react.md#installation) at the top level for this to work. + +Here is an example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + +
+

Hello

+

Thanks for trying out our feature flags.

+
+
+ ) +} +``` + +- The `match` on the component can be either `true`, or the variant key, to match on a specific variant. + +- If you also want to show a default message, you can pass these in the `fallback` attribute. + +If you wish to customise logic around when the component is considered visible, you can pass in `visibilityObserverOptions` to the feature. These take the same options as the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). By default, we use a threshold of 0.1. + +#### Payloads + +If your flag has a payload, you can pass a function to children whose first argument is the payload. For example: + +React + +PostHog AI + +```jsx +import { PostHogFeature } from '@posthog/react' +function App() { + return ( + + {(payload) => { + return ( +
+

{payload.welcomeMessage}

+

Thanks for trying out our feature flags.

+
+ ) + }} +
+ ) +} +``` + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds). +} +) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +JavaScript + +PostHog AI + +```javascript +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +## Node.js + +There are two steps to implement feature flags in Node: + +### Step 1: Evaluate flags once + +Call `client.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +#### Multivariate feature flags + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +const enabledVariant = flags.getFlag('flag-key') +if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + const matchedFlagPayload = flags.getFlagPayload('flag-key') +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `undefined` when the flag wasn't returned by the evaluation. + +> **Note:** `client.isFeatureEnabled()`, `client.getFeatureFlag()`, `client.getFeatureFlagPayload()`, and `capture({ sendFeatureFlags: true })` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user') +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Node.js + +PostHog AI + +```javascript +// Attach only flags accessed with isEnabled() or getFlag() before this call +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.onlyAccessed(), +}) +// Attach only specific flags +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Node.js + +PostHog AI + +```javascript +client.capture({ + distinctId: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_your_user', { + flagKeys: ['checkout-flow', 'new-dashboard'], +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Node.js + +PostHog AI + +```javascript +const flags = await client.evaluateFlags('distinct_id_of_the_user', { + personProperties: { + property_name: 'value', + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + groupProperties: { + your_group_type: { + group_property_name: 'value', + }, + another_group_type: { + group_property_name: 'value', + }, + }, +}) +if (flags.isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +JavaScript + +PostHog AI + +```javascript +const client = new PostHog('', { + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). +}) +``` + +## Python + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +## PHP + +There are two steps to implement feature flags in PHP: + +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +#### Multivariate feature flags + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +$enabledVariant = $flags->getFlag('flag-key'); +if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + $matchedFlagPayload = $flags->getFlagPayload('flag-key'); +} +``` + +`$flags->getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +You can also call `$flags->getKeys()` to list the evaluated flag keys, or `$flags->getEventProperties()` to get the `$feature/` and `$active_feature_flags` properties that would be attached to a captured event. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags('distinct_id_of_your_user'); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, +]); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +PHP + +PostHog AI + +```php +// Attach only flags accessed with isEnabled() or getFlag() before this call +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->onlyAccessed(), +]); +// Attach only specific flags +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), +]); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +PHP + +PostHog AI + +```php +PostHog::capture([ + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => [ + // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key' => 'variant-key', + ], +]); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Optional evaluation parameters + +`evaluateFlags()` also accepts optional parameters for local evaluation and GeoIP behavior: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_your_user', + groups: ['company' => 'company_id_in_your_db'], + personProperties: ['plan' => 'pro'], + groupProperties: ['company' => ['employees' => 11]], + onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback. + disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation. + flagKeys: ['checkout-flow', 'new-dashboard'], +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `$flags->isEnabled()` or `$flags->getFlag()` for a flag. + +The SDK deduplicates these events per `(flag key, distinct_id)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`$flags->getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PHP + +PostHog AI + +```php +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id', + ], + personProperties: ['property_name' => 'value'], + groupProperties: [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'], + ], +); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_ms` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +PHP + +PostHog AI + +```php +PostHog::init("", + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds). + ] +); +``` + +## Ruby + +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +#### Multivariate feature flags + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +enabled_variant = flags.get_flag('flag-key') +if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload('flag-key') +end +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_result()`, `posthog.get_feature_flag_payload()`, and `capture({ ..., send_feature_flags: true })` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags('distinct_id_of_your_user') +if flags.enabled?('flag-key') + # Do something differently for this user +end +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Ruby + +PostHog AI + +```ruby +# Attach only flags accessed with enabled?() or get_flag() before this call +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only_accessed, +}) +# Attach only specific flags +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + flags: flags.only(['checkout-flow', 'new-dashboard']), +}) +``` + +`only_accessed` is order-dependent. If you call it before accessing any flags with `enabled?()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Ruby + +PostHog AI + +```ruby +posthog.capture({ + distinct_id: 'distinct_id_of_your_user', + event: 'event_name', + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + flag_keys: ['checkout-flow', 'new-dashboard'], +) +``` + +### Evaluating locally only + +If you want to skip the remote `/flags` request and only use locally cached definitions, pass `only_evaluate_locally: true`: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + only_evaluate_locally: true, +) +``` + +### Disabling GeoIP for flag evaluation + +Pass `disable_geoip: true` to disable GeoIP lookup for remote flag evaluation: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_your_user', + disable_geoip: true, +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.enabled?()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Ruby + +PostHog AI + +```ruby +flags = posthog.evaluate_flags( + 'distinct_id_of_the_user', + person_properties: { + property_name: 'value' + }, + groups: { + your_group_type: 'your_group_id', + another_group_type: 'your_group_id', + }, + group_properties: { + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, + }, +) +if flags.enabled?('flag-key') + # Do something differently for this user +end +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flag_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Ruby + +PostHog AI + +```ruby +posthog = PostHog::Client.new({ + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. +}) +``` + +## Go + +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once + +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +#### Multivariate feature flags + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error (e.g. capture error and fallback to default behavior) +} +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") +} +``` + +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, +}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Go + +PostHog AI + +```go +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.OnlyAccessed(), +}) +// Attach only specific flags +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags.Only([]string{"checkout-flow", "new-dashboard"}), +}) +``` + +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Go + +PostHog AI + +```go +client.Enqueue(posthog.Capture{ + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Properties: posthog.NewProperties(). + Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant +}) +``` + +### Evaluating only specific flags + +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_your_user", + FlagKeys: []string{"checkout-flow", "new-dashboard"}, +}) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlags()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Go + +PostHog AI + +```go +flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{ + DistinctId: "distinct_id_of_the_user", + Groups: posthog.NewGroups(). + Set("your_group_type", "your_group_id"). + Set("another_group_type", "your_group_id"), + PersonProperties: posthog.NewProperties(). + Set("property_name", "value"), + GroupProperties: map[string]posthog.Properties{ + "your_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + "another_group_type": posthog.NewProperties(). + Set("group_property_name", "value"), + }, +}) +if err != nil { + // Handle error +} +if flags.IsEnabled("flag-key") { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `FeatureFlagRequestTimeout` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Go + +PostHog AI + +```go +// import "time" +client, _ := posthog.NewWithConfig( + os.Getenv(""), + posthog.Config{ + PersonalApiKey: "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower. + Endpoint: "https://us.i.posthog.com", + FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds. + }, +) +``` + +## React Native + +There are two ways to implement feature flags in React Native: + +1. Using hooks. +2. Loading the flag directly. + +### Method 1: Using hooks + +#### Example 1: Boolean feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const booleanFlag = useFeatureFlag('key-for-your-boolean-flag') + if (booleanFlag === undefined) { + // the response is undefined if the flags are being loaded + return null + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return booleanFlag ? Testing feature 😄 : Not Testing feature 😢 +} +``` + +#### Example 2: Multivariate feature flags + +React Native + +PostHog AI + +```jsx +import { useFeatureFlag } from 'posthog-react-native' +const MyComponent = () => { + const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag') + if (multiVariantFeature === undefined) { + // the response is undefined if the flags are being loaded + return null + } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant + // Do something + } + // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload + return
+} +``` + +### Method 2: Loading the flag directly + +React Native + +PostHog AI + +```jsx +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.isFeatureEnabled('key-for-your-boolean-flag') +// Defaults to undefined if not loaded yet or if there was a problem loading +posthog.getFeatureFlag('key-for-your-boolean-flag') +// Multivariant feature flags are returned as a string +posthog.getFeatureFlag('key-for-your-multivariate-flag') +// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading) +posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +React Native + +PostHog AI + +```jsx +for (const flag of posthog.getAllFeatureFlags()) { + console.log(flag.key, flag.enabled, flag.variant, flag.payload) +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately — **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +React Native + +PostHog AI + +```jsx +posthog.onFeatureFlags((flags) => { + // feature flags are guaranteed to be available at this point + if (posthog.isFeatureEnabled('flag-key')) { + // do something + } +}) +``` + +### Reloading flags + +PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag. + +If want to manually trigger a refresh, you can call `reloadFeatureFlagsAsync()`: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags)) +``` + +Or when you want to trigger the reload, but don't care about the result: + +React Native + +PostHog AI + +```jsx +posthog.reloadFeatureFlags() +``` + +### Feature flag caching + +The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means **inactive users may see stale flag values** from their last session. + +For example, if a user last opened your app when a flag was `false`, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached `false` first, then fetches the fresh `true` value from the API. + +To ensure fresh flag values: + +React Native + +PostHog AI + +```jsx +// Force refresh on app start +await posthog.reloadFeatureFlagsAsync() +``` + +Or clear cached values for inactive users: + +React Native + +PostHog AI + +```jsx +if (lastActiveDate < migrationDate) { + posthog.reset() // Clears all cached data +} +``` + +### Request timeout + +You can configure the `featureFlagsRequestTimeoutMs` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds. + +React Native + +PostHog AI + +```jsx +export const posthog = new PostHog('', { + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + host: 'https://us.i.posthog.com', + featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds). +}) +``` + +### Error handling + +When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler: + +React Native + +PostHog AI + +```jsx +function handleFeatureFlag(client, flagKey, distinctId) { + try { + const isEnabled = client.isFeatureEnabled(flagKey, distinctId); + console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`); + return isEnabled; + } catch (error) { + console.error(`Error fetching feature flag '${flagKey}': ${error.message}`); + // Optionally, you can return a default value or throw the error + // return false; // Default to disabled + throw error; + } +} +// Usage example +try { + const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123'); + if (flagEnabled) { + // Implement new feature logic + } else { + // Implement old feature logic + } +} catch (error) { + // Handle the error at a higher level + console.error('Feature flag check failed, using default behavior'); + // Implement fallback logic +} +``` + +### Overriding server properties + +Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}) +``` + +Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation. + +Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading: + +React Native + +PostHog AI + +```jsx +posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false) +``` + +At any point, you can reset these properties by calling `resetPersonPropertiesForFlags`: + +React Native + +PostHog AI + +```jsx +posthog.resetPersonPropertiesForFlags() +``` + +The same holds for [group](/docs/product-analytics/group-analytics.md) properties: + +React Native + +PostHog AI + +```jsx +// set properties for a group +posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}}) +// reset properties for all groups: +posthog.resetGroupPropertiesForFlags() +``` + +> **Note:** You don't need to add the group names here, since these properties are automatically attached to the current group (set via `posthog.group()`). When you change the group, these properties are reset. + +**Automatic overrides** + +Whenever you call `posthog.identify` with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call `posthog.group()`. + +**Default overridden properties** + +By default, we always override some properties based on the user IP address. + +The list of properties that this overrides: + +1. $geoip\_city\_name +2. $geoip\_country\_name +3. $geoip\_country\_code +4. $geoip\_continent\_name +5. $geoip\_continent\_code +6. $geoip\_postal\_code +7. $geoip\_time\_zone + +This enables any geolocation-based flags to work without manually setting these properties. + +## Android + +### Boolean feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") +} +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback to wait for the feature flag request to finish: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +import com.posthog.android.PostHogAndroidConfig +import com.posthog.PostHogOnFeatureFlags +// During SDK initialization +val config = PostHogAndroidConfig(apiKey = "").apply { + onFeatureFlags = PostHogOnFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } + } +} +// And/or after the SDK is initialized +PostHog.reloadFeatureFlags { + if (PostHog.isFeatureEnabled("flag-key")) { + // do something + } +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.reloadFeatureFlags() +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +## iOS + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +## Flutter + +### Boolean feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.enabled) { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Multivariate feature flags + +Dart + +PostHog AI + +```dart +final result = await Posthog().getFeatureFlagResult('flag-key'); +if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + final matchedFlagPayload = result.payload; +} +``` + +### Ensuring flags are loaded before usage + +> To use the `onFeatureFlags` callback, you must [set up the SDK manually](#installation). On Android and iOS, disable `com.posthog.posthog.AUTO_INIT` first. + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `onFeatureFlags` callback in your config to be notified when flags are loaded: + +Dart + +PostHog AI + +```dart +final config = PostHogConfig(''); +config.host = 'https://us.i.posthog.com'; +config.onFeatureFlags = () async { + if (await Posthog().isFeatureEnabled('flag-key')) { + // do something + } +}; +await Posthog().setup(config); +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Dart + +PostHog AI + +```dart +await Posthog().reloadFeatureFlags(); +``` + +## Java + +There are two steps to implement feature flags in Java: + +### Step 1: Evaluate flags once + +Call `posthog.evaluateFlags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +Object flagValue = flags.getFlag("flag-key"); +String enabledVariant = flagValue instanceof String ? (String) flagValue : null; +if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```java +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags) + .build() +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```java +// Attach only flags accessed with isEnabled() or getFlag() before this call +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.onlyAccessed()) + .build() +); +// Attach only specific flags +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .flags(flags.only("checkout-flow", "new-dashboard")) + .build() +); +``` + +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Java + +PostHog AI + +```java +posthog.capture( + "distinct_id_of_your_user", + "event_name", + PostHogCaptureOptions.builder() + .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant + .build() +); +``` + +### Evaluating only specific flags + +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: + +Java + +PostHog AI + +```java +import java.util.Arrays; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_your_user", + PostHogEvaluateFlagsOptions.builder() + .flagKeys(Arrays.asList("checkout-flow", "new-dashboard")) + .build() +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluateFlags()`, the SDK sends this event when you call `flags.isEnabled()` or `flags.getFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.getFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `onlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Java + +PostHog AI + +```java +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( + "distinct_id_of_the_user", + PostHogEvaluateFlagsOptions.builder() + .group("your_group_type", "your_group_id") + .group("another_group_type", "your_group_id") + .groupProperty("your_group_type", "group_property_name", "value") + .groupProperty("another_group_type", "group_property_name", "value") + .personProperty("property_name", "value") + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## Rust + +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once + +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); +} +``` + +#### Multivariate feature flags + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, FlagValue}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +match flags.get_flag("flag-key") { + Some(FlagValue::String(variant)) if variant == "variant-key" => { + // Do something differently for this user + // Optional: fetch the payload + let matched_flag_payload = flags.get_flag_payload("flag-key"); + } + _ => {} +} +``` + +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Rust + +PostHog AI + +```rust +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).await.unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Rust + +PostHog AI + +```rust +// Attach only flags accessed with is_enabled() or get_flag() before this call +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only_accessed()); +client.capture(event); +// Attach only specific flags +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"])); +client.capture(event); +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Rust + +PostHog AI + +```rust +use posthog_rs::Event; +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap(); +client.capture(event); +``` + +### Evaluating only specific flags + +By default, `evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions { + flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]), + ..Default::default() + }, +).await.unwrap(); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Blocking client + +If you're using the blocking client (with `default-features = false`), the API is the same but without `.await`: + +Rust + +PostHog AI + +```rust +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), +).unwrap(); +if flags.is_enabled("flag-key") { + // Do something differently for this user +} +``` + +## Elixir + +There are two steps to implement feature flags in Elixir: + +### Step 1: Evaluate flags once + +Call `PostHog.FeatureFlags.evaluate_flags/1` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +#### Multivariate feature flags + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key") +if enabled_variant == "variant-key" do + # Do something differently for this user + # Optional: fetch the payload + payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key") +end +``` + +`PostHog.FeatureFlags.Evaluations.get_flag/2` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`, `PostHog.FeatureFlags.get_feature_flag_result/2`, and `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the migration period, but they're deprecated. Prefer `evaluate_flags/1` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Put the evaluated flags snapshot in context + +Put the same `snapshot` object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another `/flags` request. + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do + # Do something differently for this user +end +PostHog.FeatureFlags.set_in_context(snapshot) +PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"}) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, put a filtered snapshot in context: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user") +# Attach only flags accessed with enabled?/2 or get_flag/2 before this call +PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only_accessed(snapshot) +) +# Or attach only specific flags +PostHog.FeatureFlags.set_in_context( + PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"]) +) +``` + +`only_accessed/1` is order-dependent. If you call it before accessing any flags with `enabled?/2` or `get_flag/2`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Elixir + +PostHog AI + +```elixir +PostHog.capture("event_name", %{ + "$feature/feature-flag-key" => "variant-key", + distinct_id: "distinct_id_of_your_user" +}) +``` + +### Evaluating only specific flags + +By default, `evaluate_flags/1` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Elixir + +PostHog AI + +```elixir +{:ok, snapshot} = + PostHog.FeatureFlags.evaluate_flags(%{ + distinct_id: "distinct_id_of_your_user", + flag_keys: ["checkout-flow", "new-dashboard"] + }) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `evaluate_flags/1`, the SDK sends this event when you call `PostHog.FeatureFlags.Evaluations.enabled?/2` or `PostHog.FeatureFlags.Evaluations.get_flag/2` for a flag. + +`PostHog.FeatureFlags.Evaluations.get_flag_payload/2` doesn't send `$feature_flag_called` events. + +## .NET + +There are two steps to implement feature flags in .NET: + +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +#### Multivariate feature flags + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +var enabledVariant = flags.GetFlag("flag-key")?.VariantKey; +if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant +{ + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); +} +``` + +`flags.GetFlag()` returns a nullable `FeatureFlag` object. Check `VariantKey` for multivariate flags and `IsEnabled` for boolean flags. It returns `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `Capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags +); +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +C# + +PostHog AI + +```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.OnlyAccessed() +); +// Attach only specific flags +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: null, + groups: null, + flags: flags.Only("checkout-flow", "new-dashboard") +); +``` + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +C# + +PostHog AI + +```csharp +posthog.Capture( + "distinct_id_of_your_user", + "event_name", + properties: new() + { + // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + ["$feature/feature-flag-key"] = "variant-key", + } +); +``` + +### Evaluating only specific flags + +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_your_user", + options: new AllFeatureFlagsOptions + { + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, + } +); +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `EvaluateFlagsAsync()`, the SDK sends this event when you call `flags.IsEnabled()` or `flags.GetFlag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +C# + +PostHog AI + +```csharp +var flags = await posthog.EvaluateFlagsAsync( + "distinct_id_of_the_user", + options: new AllFeatureFlagsOptions + { + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { + new Group("your_group_type", "your_group_id") + { + ["group_property_name"] = "value", + }, + new Group("another_group_type", "another_group_id") + { + ["group_property_name"] = "another value", + }, + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +## API + +There are 3 steps to implement feature flags using the PostHog API: + +### Step 1: Evaluate the feature flag value using `flags` + +`flags` is the endpoint used to determine if a given flag is enabled for a certain user or not. + +#### Request + +PostHog AI + +### Terminal + +```shell +# Basic request (flags only) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2" +# With configuration (flags + PostHog config) +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { + "group_type": "group_id" + } +}' "https://us.i.posthog.com/flags?v=2&config=true" +``` + +### Python + +```python +import requests +import json +# Basic request (flags only) +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "groups": { + "group_type": "group_id" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +# With configuration (flags + PostHog config) +url_with_config = "https://us.i.posthog.com/flags?v=2&config=true" +response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload)) +print(response_with_config.json()) +``` + +### Node.js + +```javascript +import fetch from "node-fetch"; +async function sendFlagsRequest() { + const headers = { + "Content-Type": "application/json", + }; + const payload = { + api_key: "", + distinct_id: "user distinct id", + groups: { + group_type: "group_id", + }, + }; + // Basic request (flags only) + const url = "https://us.i.posthog.com/flags?v=2"; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + console.log(data); + // With configuration (flags + PostHog config) + const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true"; + const responseWithConfig = await fetch(urlWithConfig, { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + }); + const dataWithConfig = await responseWithConfig.json(); + console.log(dataWithConfig); +} +sendFlagsRequest(); +``` + +> **Note:** The `groups` key is only required for group-based feature flags. If you use it, replace `group_type` and `group_id` with the values for your group such as `company: "Twitter"`. + +#### Using evaluation context tags and runtime filtering without SDKs + +When making direct API calls to the `/flags` endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering. + +##### Evaluation contexts + +To filter flags by evaluation context, include the `evaluation_contexts` field in your request body: + +> **Note:** The legacy parameter `evaluation_environments` is also supported for backward compatibility. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "evaluation_contexts": ["production", "web"] +}' "https://us.i.posthog.com/flags?v=2" +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "user distinct id", + "evaluation_contexts": ["production", "web"] +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### JavaScript + +```javascript +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-distinct-id", + evaluation_contexts: ["production", "web"] + }), +}); +const data = await response.json(); +``` + +Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example: + +- Flag with evaluation context tags `["production", "api", "backend"]` + request with `["production", "web"]` = ✅ Flag evaluates ("production" matches) +- Flag with evaluation context tags `["staging", "api"]` + request with `["production", "web"]` = ❌ Flag doesn't evaluate (no tags match) +- Flag with evaluation context tags `["web", "mobile"]` + request with `["production", "web"]` = ✅ Flag evaluates ("web" matches) +- Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility) + +##### Runtime detection + +Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all). + +**How runtime is detected:** + +1. **User-Agent patterns** - The system analyzes the User-Agent header: + + - **Client-side patterns**: `Mozilla/`, `Chrome/`, `Safari/`, `Firefox/`, `Edge/` (browsers), or mobile SDKs like `posthog-android/`, `posthog-ios/`, `posthog-react-native/`, `posthog-flutter/` + - **Server-side patterns**: `posthog-python/`, `posthog-ruby/`, `posthog-php/`, `posthog-java/`, `posthog-go/`, `posthog-node/`, `posthog-dotnet/`, `posthog-elixir/`, `python-requests/`, `curl/` +2. **Browser-specific headers** - Presence of these headers indicates client-side: + + - `Origin` header + - `Referer` header + - `Sec-Fetch-Mode` header + - `Sec-Fetch-Site` header +3. **Default behavior** - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all" + +**Examples of runtime detection:** + +JavaScript + +PostHog AI + +```javascript +// Browser fetch - Detected as CLIENT runtime +// Will receive: client-only flags + "all" flags +// Won't receive: server-only flags +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser automatically adds Origin, Referer, Sec-Fetch-* headers + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +Python + +PostHog AI + +```python +# Python requests - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +import requests +response = requests.post( + "https://us.i.posthog.com/flags?v=2", + json={ + "api_key": "", + "distinct_id": "user-id" + } + # python-requests/ in User-Agent indicates server-side +) +``` + +Terminal + +PostHog AI + +```shell +# curl - Detected as SERVER runtime +# Will receive: server-only flags + "all" flags +# Won't receive: client-only flags +curl -v -L --header "Content-Type: application/json" -d '{ + "api_key": "", + "distinct_id": "user-id" +}' "https://us.i.posthog.com/flags?v=2" +# curl/ in User-Agent indicates server-side +``` + +JavaScript + +PostHog AI + +```javascript +// Node.js with custom User-Agent - Control runtime detection +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "posthog-node/3.0.0" // Explicitly indicates server-side + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id" + }) +}); +``` + +##### Combining evaluation context tags and runtime filtering + +Both features work together as sequential filters: + +JavaScript + +PostHog AI + +```javascript +// Example: Production web client +const response = await fetch("https://us.i.posthog.com/flags?v=2", { + method: "POST", + headers: { + "Content-Type": "application/json", + // Browser headers will trigger client runtime detection + }, + body: JSON.stringify({ + api_key: "", + distinct_id: "user-id", + evaluation_contexts: ["production", "web"] + }) +}); +// This request will only receive flags that: +// 1. Have runtime set to "client" OR "all" (due to browser headers) +// AND +// 2. Have evaluation context tags matching "production" OR "web" (or no tags) +// Note: You can also use the legacy "evaluation_environments" parameter +``` + +This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended. + +#### Response + +The response varies depending on whether you include the `config=true` query parameter: + +##### Basic response (`/flags?v=2`) + +Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results. + +> **Note:** If a feature flag is associated with an experiment that has a [holdout group](/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format `holdout-{holdout_id}` (e.g., `holdout-727`). You can detect holdout users by checking if the variant starts with `holdout-`. + +JSON + +PostHog AI + +```json +{ + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + }, + "errorsWhileComputingFlags": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +##### Full response with configuration (`/flags?v=2&config=true`) + +Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "errorsWhileComputingFlags": false, + "isAuthenticated": false, + "requestId": "550e8400-e29b-41d4-a716-446655440000", + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": { + "my-awesome-flag": { + "key": "my-awesome-flag", + "enabled": true, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Condition set 1 matched" + }, + "metadata": { + "id": 1, + "version": 1, + "payload": "{\"example\": \"json\", \"payload\": \"value\"}" + } + }, + "my-multivariate-flag" :{ + "key":"my-multivariate-flag", + "enabled": true, + "variant": "some-string-value", + "reason": { + "code": "condition_match", + "condition_index": 1, + "description": "Condition set 2 matched" + }, + "metadata": { + "id": 2, + "version": 42, + } + }, + "flag-thats-not-on": { + "key": "flag-thats-not-on", + "enabled": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0, + "description": "No condition sets matched" + }, + "metadata": { + "id": 3, + "version": 1 + } + } + } +} +``` + +> **Note:** `errorsWhileComputingFlags` will return `true` if we didn't manage to compute some flags (for example, if there's an [ongoing incident involving flag evaluation](https://status.posthog.com/)). +> +> This enables partial updates to currently active flags in your clients. + +#### Quota limiting + +If your organization exceeds its feature flag quota, the `/flags` endpoint will return a modified response with `quotaLimited`. + +For basic response (`/flags?v=2`): + +JSON + +PostHog AI + +```json +{ + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" +} +``` + +For full response with configuration (`/flags?v=2&config=true`): + +JSON + +PostHog AI + +```json +{ + "config": { + "enable_collect_everything": true + }, + "toolbarParams": {}, + "isAuthenticated": false, + "supportedCompression": [ + "gzip", + "lz64" + ], + "flags": {}, + "errorsWhileComputingFlags": false, + "quotaLimited": ["feature_flags"], + "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e" + // ... other fields, not relevant to feature flags +} +``` + +When you receive a response with `quotaLimited` containing `"feature_flags"`, it means: + +1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota +2. If you want to continue evaluating feature flags, you can increase your quota in [your billing settings](https://us.posthog.com/organization/billing) under **Feature flags & Experiments** or [contact support](https://us.posthog.com/#panel=support%3Asupport%3Abilling%3A%3Atrue) + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +To do this, include the `$feature/feature_flag_name` property in your event: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "your_event_name", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Step 3: Send a `$feature_flag_called` event + +To track usage of your feature flag and view related analytics in PostHog, submit the `$feature_flag_called` event whenever you check a feature flag value in your code. + +You need to include two properties with this event: + +1. `$feature_flag_response`: This is the name of the variant the user has been assigned to e.g., "control" or "test" +2. `$feature_flag`: This is the key of the feature flag in your experiment. + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "event": "$feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +}' https://us.i.posthog.com/i/v0/e/ +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/i/v0/e/" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "event": "feature_flag_called", + "distinct_id": "distinct_id_of_your_user", + "properties": { + "$feature_flag": "feature-flag-key", + "$feature_flag_response": "variant-name" + } +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response) +``` + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +PostHog AI + +### Terminal + +```shell +curl -v -L --header "Content-Type: application/json" -d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user", + "groups" : { # Required only for group-based feature flags + "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group. + }, + "person_properties": {"": ""}, # Optional. Include any properties used to calculate the value of the feature flag. + "group_properties": {"group type": {"":""}} # Optional. Include any properties used to calculate the value of the feature flag. +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the `HTTP_X_FORWARDED_FOR` when making your `/flags` request: + +PostHog AI + +### Terminal + +```shell +curl -v -L \ +--header "Content-Type: application/json" \ +--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \ +-d ' { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +}' https://us.i.posthog.com/flags?v=2 +``` + +### Python + +```python +import requests +import json +url = "https://us.i.posthog.com/flags?v=2" +headers = { + "Content-Type": "application/json", + "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use" +} +payload = { + "api_key": "", + "distinct_id": "distinct_id_of_your_user" +} +response = requests.post(url, headers=headers, data=json.dumps(payload)) +print(response.json()) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-wordpress/references/best-practices.md b/skills/posthog/all/skills/feature-flags-wordpress/references/best-practices.md new file mode 100644 index 00000000..7831a883 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/references/best-practices.md @@ -0,0 +1,237 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Best practices for production-ready flags - Docs + +Copy page + +# Best practices for production-ready flags - Docs + +## Checklist + +- [Call `identify()` before evaluating flags](#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem. +- [Evaluate flags server-side with local evaluation](#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds. +- [Bootstrap client-side flags](#have-the-value-before-you-need-it) – client-side evaluation is async. [Bootstrap](/docs/feature-flags/bootstrapping.md) to eliminate the gap. +- [Handle `undefined` explicitly](#undefined-is-not-false) – it means "not evaluated yet," not `false`. +- [Evaluate once, record the result](#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes. +- [Evaluate where the data lives](#evaluate-where-the-data-lives) – if the data is on your server, evaluate there. +- [Choose evaluation context deliberately](#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag. +- [Clean up flags that have done their job](#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it. +- [Disable client-side evaluation for server-side flags](#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided. +- [Use a reverse proxy](#use-a-reverse-proxy) – prevent ad blockers from disabling your flags. +- [Call your flag in as few places as possible](#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places. +- [Name flags clearly](#name-flags-clearly) – descriptive names, types, positive language. +- [Roll out progressively](#roll-out-progressively) – start small, monitor, then increase. + +**The mental model:** [Flags are pure functions](#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. [Unexpected results are almost always input problems](#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed. + +--- + +## Flags are pure functions + +A flag hashes two things – the **flag key** and the **distinct ID** – and returns a deterministic result. Same inputs, same output. Every time. + +PostHog AI + +``` +hash("my-experiment", "user-123") → 0.31 → always 0.31 +``` + +On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: **same flag key + same distinct ID = same result.** + +**Technically** + +"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on `flag_key` + `distinct_id`. Some features like [experience continuity](#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output. + +### How the hash works + +PostHog uses SHA-1: + +PostHog AI + +``` +hash_key = "{flag_key}.{distinct_id}" +position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE → float in [0, 1] +in_rollout = position <= rollout_percentage / 100 +``` + +For variants, a second hash with salt `"variant"` maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags. + +If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns `false`. + +## Unexpected results are almost always input problems + +If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays. + +So when a flag returns something you didn't expect, **the flag is fine, the problem is in the inputs passed to the flag.** Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem. + +If you keep running into flag issues and they're not [incidents](https://status.posthog.com), the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it. + +We're here to help with that – this guide, [PostHog AI](/docs/feature-flags/manage-flags-ai.md), and [professional services](https://posthog.com/professional-services) all exist for exactly this. But the starting point is always the same: **look at the inputs.** + +When something goes wrong, in order of likelihood: + +1. **Input problems** (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – [bootstrapping](/docs/feature-flags/bootstrapping.md), [property overrides](/docs/feature-flags/property-overrides.md), [server-side evaluation](/docs/feature-flags/local-evaluation.md). +2. **Output problems.** The flag returned the right value but your code misread it – `undefined` treated as `false`, no handling for the loading gap, evaluating repeatedly instead of recording the result. +3. **Actual incidents.** Check [status.posthog.com](https://status.posthog.com). If nothing there, it's #1 or #2. And even here: with [server-side local evaluation](/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close. + +## Resolve identity before evaluating flags + +Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person. + +If you call `identify()` after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After `identify()`, the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed. + +Call [`identify()`](/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, [bootstrap](/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See [keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) for the full picture. + +**SPA-specific timing.** In single-page applications, `identify()` and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the `distinct_id` synchronously when `identify()` runs, but if `capture()` was called first in the same execution frame, that event uses the anonymous ID. The fix: call `identify()` before the navigation that mounts post-auth components – in Vue, in `beforeEach` before `next()`; in React, before `navigate()`, not in a `useEffect` inside the target route. + +### Don't rely on flag persistence to fix identity gaps + +If you've enabled [experience continuity](/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it. + +That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of [known bugs](https://github.com/PostHog/posthog-js/issues/2623) where values can still change after `identify()`. It also means no support for [local evaluation](/docs/feature-flags/local-evaluation.md) and slower flag responses. + +The better fix is to make persistence unnecessary. Use [device bucketing](/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID [never changes](/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper [identity resolution](/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely. + +## Evaluation architecture + +How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time. + +### Evaluate once, not continuously + +A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes. + +Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs. + +- **Feature rollouts** – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer. +- **Experiments** – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to. + +### Evaluate where the data lives + +If you target a flag on `plan_type: "pro"`, your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data. + +If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data. + +If you must evaluate client-side, use [`setPersonPropertiesForFlags()`](/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser. + +Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on. + +### Server-side local evaluation is the recommended default + +[Server-side local evaluation](/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible: + +- **All inputs are explicit.** You pass the distinct ID and properties directly. When something's wrong, you log what you passed. +- **Your data is right there.** User plan, account type, permissions – it's in your database at request time. No syncing, no fetching. +- **No workarounds needed.** Client-side evaluation often requires `setPersonPropertiesForFlags()`, `onFeatureFlags()`, and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap. + +Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap. + +### Have the value before you need it + +Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns `undefined`, not `false`. + +**[Bootstrap](/docs/feature-flags/bootstrapping.md) is the fix.** Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker. + +If you can't bootstrap, use `onFeatureFlags()` to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay. + +### `undefined` is not "flag is off" nor `false` + +`posthog.getFeatureFlag()` returns `undefined` before flags load. That means "not evaluated yet," not "flag is off." + +JavaScript + +PostHog AI + +```javascript +// Returns undefined before flags load – not false +if (posthog.getFeatureFlag('my-experiment') === 'test') { + // Never runs during the loading gap +} +``` + +Handle it with [bootstrap](/docs/feature-flags/bootstrapping.md) (preferred) or `onFeatureFlags()` (adds a loading state). You can check the current identity with `posthog.get_distinct_id()`. + +The "not loaded yet" return value varies across SDKs – some return `undefined`/`nil`/`None`, others return `false` or a `defaultValue` you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of `getFeatureFlag()` and `isFeatureEnabled()` when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the [Feature Flags API](/docs/api/feature-flags.md) to query flag definitions directly. + +## Flag hygiene + +Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient. + +### Choose a flag type intentionally + +Every flag in PostHog is configured as client-side, server-side, or both via [evaluation contexts](/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation. + +If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server. + +Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed. + +### Clean up flags that have done their job + +A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable `/flags` requests, keep appearing in SDK payloads, and add clutter to your codebase. + +Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See [cleaning up stale flags](/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and [cutting costs](/docs/feature-flags/cutting-costs.md) for more on reducing your bill. + +**An idea worth considering:** design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's `true`, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user `/flags` requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to `false`. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup. + +### Disable client-side evaluation for server-side flags + +If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did. + +This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size. + +### Use a reverse proxy + +Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a [reverse proxy](/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free [managed reverse proxy](/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own. + +### Call your flag in as few places as possible + +The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function: + +JavaScript + +PostHog AI + +```javascript +function useBetaFeature() { + return posthog.isFeatureEnabled('beta-feature') +} +``` + +### Name flags clearly + +Good naming makes flags easier to understand and maintain: + +- **Use descriptive names.** `is_v2_billing_dashboard_enabled` is clearer than `is_dashboard_enabled`. +- **Use name types.** Suffix with the purpose: `new-billing-experiment`, `new-billing-release`. +- **Reflect the return type.** `is_premium_user` for a boolean, `selected_theme` for a string. +- **Use positive language for booleans.** `is_premium_user` instead of `is_not_premium_user` – avoids double negatives. + +### Roll out progressively + +Start at 5-10% of users, monitor metrics, then gradually increase. This is a [phased rollout](/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone. + +### Use dependencies for complex rollouts + +[Feature flag dependencies](/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies. + +### Be careful with "Latest" person properties + +PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like `$current_url`) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., `first_landing_page` via `$set_once`) and target that instead. + +### Reducing your bill + +Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our [dedicated guide to cutting costs](/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill. + +## Further reading + +- [Identity resolution](/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is +- [Keeping flag evaluations stable](/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions +- [Local evaluation](/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control +- [Bootstrapping](/docs/feature-flags/bootstrapping.md) – having flag values before the page renders + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/feature-flags-wordpress/references/php.md b/skills/posthog/all/skills/feature-flags-wordpress/references/php.md new file mode 100644 index 00000000..37699363 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/references/php.md @@ -0,0 +1,193 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# PHP Feature Flags installation - Docs + +Copy page + +# PHP Feature Flags installation - Docs + +1. 1 + + ## Install the package + + Required + + Install the PostHog PHP library using Composer: + + Terminal + + PostHog AI + + ```bash + composer require posthog/posthog-php + ``` + +2. 2 + + ## Configure PostHog + + Required + + Initialize the PostHog client with your project token and host: + + PHP + + PostHog AI + + ```php + PostHog\PostHog::init( + '', + ['host' => 'https://us.i.posthog.com'] + ); + ``` + +3. 3 + + ## Send events + + Recommended + + Once installed, you can manually send events to test your integration: + + PHP + + PostHog AI + + ```php + PostHog::capture([ + 'distinctId' => 'test-user', + 'event' => 'test-event', + ]); + ``` + +4. 4 + + ## Evaluate boolean feature flags + + Required + + Check if a feature flag is enabled: + + ```php + $isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') + if ($isMyFlagEnabledForUser) { + // Do something differently for this user + } + ``` + +5. 5 + + ## Evaluate multivariate feature flags + + Optional + + For multivariate flags, check which variant the user has been assigned: + + ```php + $enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user') + if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant + # Do something differently for this user + } + ``` + +6. 6 + + ## Include feature flag information in events + + Required + + If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + + **Note:** This step is only required for events captured using our server-side SDKs or API. + + ## Set send_feature_flags (recommended) + + Set `send_feature_flags` to `true` in your capture call: + + PHP + + PostHog AI + + ```php + PostHog::capture(array( + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'send_feature_flags' => true + )); + ``` + + ## Include $feature property + + Include the `$feature/feature_flag_name` property in your event properties: + + PHP + + PostHog AI + + ```php + PostHog::capture(array( + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'properties' => array( + '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant + ) + )); + ``` + +7. 7 + + ## Override server properties + + Optional + + Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with: + + ```php + PostHog::getFeatureFlag( + 'flag-key', + 'distinct_id_of_the_user', + [ + 'your_group_type' => 'your_group_id', + 'another_group_type' => 'your_group_id' + ], // groups + ['property_name' => 'value'], // person properties + [ + 'your_group_type' => ['group_property_name' => 'value'], + 'another_group_type' => ['group_property_name' => 'value'] + ], // group properties + false, // onlyEvaluateLocally, Optional. Defaults to false. + true // sendFeatureFlagEvents + ) + ``` + +8. 8 + + ## Running experiments + + Optional + + Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard. + +9. 9 + + ## Next steps + + Recommended + + Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform. + + | Resource | Description | + | --- | --- | + | [Creating a feature flag](/docs/feature-flags/creating-feature-flags.md) | How to create a feature flag in PostHog | + | [Adding feature flag code](/docs/feature-flags/adding-feature-flag-code.md) | How to check flags in your code for all platforms | + | [Framework-specific guides](/docs/feature-flags/tutorials.md#framework-guides) | Setup guides for React Native, Next.js, Flutter, and other frameworks | + | [How to do a phased rollout](/tutorials/phased-rollout.md) | Gradually roll out features to minimize risk | + | [More tutorials](/docs/feature-flags/tutorials.md) | Other real-world examples and use cases | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file From 9ec5c82b6257bd5e2e84732358180d72bd956054 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:30:22 -0400 Subject: [PATCH 04/13] Update generated plugins from context-mill (mirror test) --- .../references/wordpress.md | 109 + .../all/skills/integration-android/SKILL.md | 20 +- .../integration-android/references/1-begin.md | 56 + .../integration-android/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 9 + .../integration-android/references/EXAMPLE.md | 2 +- .../integration-android/references/android.md | 300 +- .../references/identify-users.md | 123 +- .../all/skills/integration-angular/SKILL.md | 34 +- .../integration-angular/references/1-begin.md | 56 + .../integration-angular/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 23 + .../integration-angular/references/EXAMPLE.md | 16 +- .../integration-angular/references/angular.md | 70 +- .../references/identify-users.md | 123 +- .../skills/integration-astro-hybrid/SKILL.md | 43 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 37 + .../references/EXAMPLE.md | 61 +- .../references/astro.md | 58 +- .../references/identify-users.md | 123 +- .../all/skills/integration-astro-ssr/SKILL.md | 44 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 34 + .../references/EXAMPLE.md | 81 +- .../integration-astro-ssr/references/astro.md | 58 +- .../references/identify-users.md | 123 +- .../skills/integration-astro-static/SKILL.md | 34 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 23 + .../references/EXAMPLE.md | 4 +- .../references/astro.md | 58 +- .../references/identify-users.md | 123 +- .../SKILL.md | 34 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 26 + .../references/EXAMPLE.md | 4 +- .../references/astro.md | 58 +- .../references/identify-users.md | 123 +- .../all/skills/integration-django/SKILL.md | 26 +- .../integration-django/references/1-begin.md | 56 + .../integration-django/references/2-edit.md | 36 + .../integration-django/references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 20 + .../integration-django/references/EXAMPLE.md | 134 +- .../integration-django/references/django.md | 121 +- .../references/identify-users.md | 123 +- .../all/skills/integration-elixir/SKILL.md | 62 + .../integration-elixir/references/1-begin.md | 56 + .../integration-elixir/references/2-edit.md | 36 + .../integration-elixir/references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 16 + .../integration-elixir/references/EXAMPLE.md | 790 +++ .../integration-elixir/references/elixir.md | 420 ++ .../references/identify-users.md | 307 ++ .../all/skills/integration-expo/SKILL.md | 23 +- .../integration-expo/references/1-begin.md | 56 + .../integration-expo/references/2-edit.md | 36 + .../integration-expo/references/3-revise.md | 22 + .../integration-expo/references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 16 + .../integration-expo/references/EXAMPLE.md | 42 +- .../references/identify-users.md | 123 +- .../references/react-native.md | 355 +- .../all/skills/integration-fastapi/SKILL.md | 20 +- .../integration-fastapi/references/1-begin.md | 56 + .../integration-fastapi/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 20 + .../integration-fastapi/references/EXAMPLE.md | 43 +- .../references/identify-users.md | 123 +- .../integration-fastapi/references/python.md | 278 +- .../all/skills/integration-flask/SKILL.md | 20 +- .../integration-flask/references/1-begin.md | 56 + .../integration-flask/references/2-edit.md | 36 + .../integration-flask/references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 18 + .../integration-flask/references/EXAMPLE.md | 44 +- .../integration-flask/references/flask.md | 88 +- .../references/identify-users.md | 123 +- .../all/skills/integration-flutter/SKILL.md | 60 + .../integration-flutter/references/1-begin.md | 56 + .../integration-flutter/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 14 + .../integration-flutter/references/EXAMPLE.md | 1829 +++++++ .../integration-flutter/references/flutter.md | 900 ++++ .../references/identify-users.md | 307 ++ .../all/skills/integration-go/SKILL.md | 61 + .../integration-go/references/1-begin.md | 56 + .../integration-go/references/2-edit.md | 36 + .../integration-go/references/3-revise.md | 22 + .../integration-go/references/4-conclude.md | 143 + .../integration-go/references/COMMANDMENTS.md | 15 + .../integration-go/references/EXAMPLE.md | 558 ++ .../skills/integration-go/references/go.md | 573 ++ .../references/identify-users.md | 307 ++ .../all/skills/integration-java/SKILL.md | 61 + .../integration-java/references/1-begin.md | 56 + .../integration-java/references/2-edit.md | 36 + .../integration-java/references/3-revise.md | 22 + .../integration-java/references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 13 + .../integration-java/references/EXAMPLE.md | 545 ++ .../references/identify-users.md | 307 ++ .../integration-java/references/java.md | 731 +++ .../integration-javascript_node/SKILL.md | 27 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 15 + .../references/identify-users.md | 123 +- .../references/node.md | 278 +- .../references/posthog-node.md | 246 +- .../integration-javascript_web/SKILL.md | 31 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 19 + .../references/identify-users.md | 123 +- .../references/js.md | 58 +- .../references/posthog-js.md | 469 +- .../all/skills/integration-kmp/SKILL.md | 51 + .../integration-kmp/references/1-begin.md | 56 + .../integration-kmp/references/2-edit.md | 36 + .../integration-kmp/references/3-revise.md | 22 + .../integration-kmp/references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 12 + .../references/identify-users.md | 307 ++ .../skills/integration-kmp/references/kmp.md | 824 +++ .../all/skills/integration-laravel/SKILL.md | 22 +- .../integration-laravel/references/1-begin.md | 56 + .../integration-laravel/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 15 + .../integration-laravel/references/EXAMPLE.md | 13 +- .../references/identify-users.md | 123 +- .../integration-laravel/references/laravel.md | 153 +- .../integration-nextjs-app-router/SKILL.md | 41 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 35 + .../references/EXAMPLE.md | 26 +- .../references/identify-users.md | 123 +- .../references/next-js.md | 118 +- .../integration-nextjs-pages-router/SKILL.md | 41 +- .../references/1-begin.md | 56 + .../references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 35 + .../references/EXAMPLE.md | 26 +- .../references/identify-users.md | 123 +- .../references/next-js.md | 118 +- .../all/skills/integration-nuxt-3-6/SKILL.md | 72 + .../references/1-begin.md | 56 + .../integration-nuxt-3-6/references/2-edit.md | 36 + .../references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 26 + .../references/EXAMPLE.md | 942 ++++ .../references/identify-users.md | 307 ++ .../references/nuxt-js-3-6.md | 280 + .../all/skills/integration-nuxt-4/SKILL.md | 42 +- .../integration-nuxt-4/references/1-begin.md | 56 + .../integration-nuxt-4/references/2-edit.md | 36 + .../integration-nuxt-4/references/3-revise.md | 22 + .../references/4-conclude.md | 143 + .../references/COMMANDMENTS.md | 26 + .../integration-nuxt-4/references/EXAMPLE.md | 4721 ++++++++++++++++- .../references/identify-users.md | 123 +- .../integration-nuxt-4/references/nuxt-js.md | 73 +- .../all/skills/integration-php/SKILL.md | 57 + .../integration-php/references/1-begin.md | 56 + 200 files changed, 26328 insertions(+), 1260 deletions(-) create mode 100644 skills/posthog/all/skills/feature-flags-wordpress/references/wordpress.md create mode 100644 skills/posthog/all/skills/integration-android/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-android/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-android/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-android/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-android/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-angular/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-angular/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-angular/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-angular/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-angular/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-astro-hybrid/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-astro-ssr/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-astro-static/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-astro-static/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-astro-static/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-astro-static/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-astro-static/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-astro-view-transitions/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-django/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-django/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-django/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-django/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-django/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-elixir/SKILL.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/EXAMPLE.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/elixir.md create mode 100644 skills/posthog/all/skills/integration-elixir/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-expo/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-expo/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-expo/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-expo/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-expo/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-fastapi/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-fastapi/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-fastapi/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-fastapi/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-fastapi/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-flask/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-flask/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-flask/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-flask/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-flask/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-flutter/SKILL.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/EXAMPLE.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/flutter.md create mode 100644 skills/posthog/all/skills/integration-flutter/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-go/SKILL.md create mode 100644 skills/posthog/all/skills/integration-go/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-go/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-go/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-go/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-go/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-go/references/EXAMPLE.md create mode 100644 skills/posthog/all/skills/integration-go/references/go.md create mode 100644 skills/posthog/all/skills/integration-go/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-java/SKILL.md create mode 100644 skills/posthog/all/skills/integration-java/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-java/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-java/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-java/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-java/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-java/references/EXAMPLE.md create mode 100644 skills/posthog/all/skills/integration-java/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-java/references/java.md create mode 100644 skills/posthog/all/skills/integration-javascript_node/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-javascript_node/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-javascript_node/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-javascript_node/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-javascript_node/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-javascript_web/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-javascript_web/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-javascript_web/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-javascript_web/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-javascript_web/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-kmp/SKILL.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-kmp/references/kmp.md create mode 100644 skills/posthog/all/skills/integration-laravel/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-laravel/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-laravel/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-laravel/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-laravel/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-nextjs-app-router/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-nextjs-pages-router/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/SKILL.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/EXAMPLE.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/identify-users.md create mode 100644 skills/posthog/all/skills/integration-nuxt-3-6/references/nuxt-js-3-6.md create mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/1-begin.md create mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/2-edit.md create mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/3-revise.md create mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/4-conclude.md create mode 100644 skills/posthog/all/skills/integration-nuxt-4/references/COMMANDMENTS.md create mode 100644 skills/posthog/all/skills/integration-php/SKILL.md create mode 100644 skills/posthog/all/skills/integration-php/references/1-begin.md diff --git a/skills/posthog/all/skills/feature-flags-wordpress/references/wordpress.md b/skills/posthog/all/skills/feature-flags-wordpress/references/wordpress.md new file mode 100644 index 00000000..660f3189 --- /dev/null +++ b/skills/posthog/all/skills/feature-flags-wordpress/references/wordpress.md @@ -0,0 +1,109 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# How to set up WordPress analytics with PostHog - Docs + +Copy page + +# How to set up WordPress analytics with PostHog - Docs + +Getting traffic, usage, and user behavior data about your [WordPress](https://www.wordpress.org/) site is simple with PostHog. Once you have that data, you can discover insights and build dashboards with our suite of dev tools. + +## How to add PostHog to your WordPress site + +The best way to add PostHog to your WordPress site depends on what version of WordPress you are using. + +All of them require you to [signup for PostHog](https://us.posthog.com/signup), get your [snippet](/docs/getting-started/install?tab=snippet.md) with your project token and instance address from [your project settings](https://us.posthog.com/project/settings#snippet), and add the PostHog snippet to your site. + +### Option 1: Use a plugin + +The first option is to use a plugin. These enable you to easily add custom code to your site's header which we can use to add the PostHog snippet. + +For **WordPress.com** users, this is also the only option. This is because you don't have access to the `header.php` or `functions.php` files. Using plugins does require their **Business** or **Commerce** plans. We also recommend this option for [WooCommerce](/docs/libraries/woocommerce.md) sites. + +Two plugin options include: + +1. WordPress.com recommends using the free [Insert Headers and Footers](https://wordpress.com/plugins/insert-headers-and-footers) plugin. + +2. If you are already using Google Tag Manager on your WordPress site with a plugin like [Site Kit](https://wordpress.org/plugins/google-site-kit/), you can add the PostHog snippet as a tag instead. See our [Google Tag Manager docs](/docs/libraries/google-tag-manager.md) for more information. + +The workflow for these is the same: + +1. Install the plugin. +2. Add the PostHog snippet to the header via the plugin. +3. Activate the plugin. + +### Option 2: Edit your theme's functions file + +[Theme functions](https://developer.wordpress.org/themes/basics/theme-functions/) enable you to add functionality to your WordPress site. This makes them a great way to add PostHog. + +To set one up for PostHog, first, find your theme's `functions.php` file. This can be found either in `app/public/wp-content/themes/` folder or in your WordPress admin under **Tools** -> **Theme Filter Editor**. + +Next, create an `add_posthog` function with your snippet like this: + +PHP + +PostHog AI + +```php +if ( ! function_exists( 'add_posthog' ) ) : +function add_posthog() { + ?> + + + + tag +add_action('wp_head', 'add_posthog', 999); +``` + +After saving your changes or clicking **Update File**, PostHog should begin to autocapture pageviews, clicks, and more. + +### Option 3: Edit your theme's header file + +If you are using an older version of WordPress, you can edit the `header.php` file directly. + +To do this, start by going to your WordPress admin and navigating to **Appearance** -> **Theme Editor**. + +Select your theme in the editor drop-down menu to the right and click the `header.php` file in the file column to the right. + +You should now see the contents of the `header.php` template file in the code editing view. It is recommended that you copy all the text/code and save it somewhere as a back-up. + +Find the closing `` in the code editor and paste the PostHog snippet before it (see above image). Finally, click the **Update File** button at the bottom to save your changes. PostHog should begin to autocapture pageviews, clicks, and more. + +To confirm PostHog is configured correctly, visit your website and then check if the events from your session appear in PostHog. + +> **Notes:** +> +> - Using the Theme Editor is very convenient, but you have to consider the potential drawbacks of having template files writable, which many prefer to disable for security purposes. Also, wrongfully editing a file may cause problems so be sure to perform appropriate backups before attempting this. +> - If your theme auto-updates, manually editing the `header.php` file may lose your settings. Making a [Child Theme](https://developer.wordpress.org/themes/advanced-topics/child-themes/) is the recommended approach. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-android/SKILL.md b/skills/posthog/all/skills/integration-android/SKILL.md index 13c3b79c..adc42e15 100644 --- a/skills/posthog/all/skills/integration-android/SKILL.md +++ b/skills/posthog/all/skills/integration-android/SKILL.md @@ -3,7 +3,7 @@ name: integration-android description: PostHog integration for Android applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog integration for Android @@ -14,20 +14,21 @@ This skill helps you add PostHog analytics to Android applications. Follow these steps in order to complete the integration: -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion ## Reference files - `references/EXAMPLE.md` - Android example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation - `references/android.md` - Android - docs - `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow The example project shows the target implementation pattern. Consult the documentation for API details. @@ -39,6 +40,7 @@ The example project shows the target implementation pattern. Consult the documen ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version - Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. - Initialize PostHog in the Application class's `onCreate()` method diff --git a/skills/posthog/all/skills/integration-android/references/1-begin.md b/skills/posthog/all/skills/integration-android/references/1-begin.md new file mode 100644 index 00000000..55f0a832 --- /dev/null +++ b/skills/posthog/all/skills/integration-android/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-android/references/2-edit.md b/skills/posthog/all/skills/integration-android/references/2-edit.md new file mode 100644 index 00000000..e5f7ffd1 --- /dev/null +++ b/skills/posthog/all/skills/integration-android/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-android/references/3-revise.md b/skills/posthog/all/skills/integration-android/references/3-revise.md new file mode 100644 index 00000000..3b07f506 --- /dev/null +++ b/skills/posthog/all/skills/integration-android/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-android/references/4-conclude.md b/skills/posthog/all/skills/integration-android/references/4-conclude.md new file mode 100644 index 00000000..1523281b --- /dev/null +++ b/skills/posthog/all/skills/integration-android/references/4-conclude.md @@ -0,0 +1,143 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Create a live PostHog dashboard named "Analytics basics (wizard)" from the events you just instrumented, then populate it with up to five insights — lead with the business-critical views: conversion funnels, churn events, and other key signals. Use the exact same event names as implemented in the code. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. + +Always create the dashboard and insights based on the intended captures, regardless of whether those events have been observed yet. An insight is a definition over event names, not a snapshot of current data: it is expected to render empty until the first events arrive, and it fills in on its own once they do. "No data ingested yet", "the events aren't in the schema", or "the query would return nothing today" are never reasons to skip or defer insights — a dashboard handed off without them is an incomplete integration, not a cautious one. + +## How to call PostHog MCP tools + +The PostHog MCP server exposes a single `exec` tool. Every PostHog operation is driven by a CLI-style command string passed in its `command` parameter — the tool may be namespaced by the host (`mcp__posthog__exec`, `mcp__posthog-wizard__exec`), but the command grammar is the same. Tool names and schemas are not predictable, so discover and inspect before you call. + +**Grammar** — run in this order: + +```text +exec({ "command": "search " }) # find tools by name/title/description; `tools` lists them all +exec({ "command": "info " }) # REQUIRED before every call — description + input schema +exec({ "command": "schema " }) # drill into a field the schema flags with a `hint` +exec({ "command": "call " }) # run the tool +``` + +Running `info ` before `call ` is mandatory, the same way you read a file before editing it. `info` returns the full schema for simple tools; for large ones it summarizes and attaches `hint` entries pointing at fields to drill into with `schema`. Dot-notation descends objects (`query.source`), array items (`series.0.properties`), and unions. Never guess the structure of a field that carries a hint — drill first. + +Every PostHog tool goes through `exec` this way — there is no separate named tool to call directly. The inner tool names and JSON payloads below are what you pass to `call`. + +**Errors** carry a suggestion and similar tool names — read it before retrying. If a name isn't found it may have been renamed; run `search ` or `tools` again to find the current one. + +Create the parent dashboard first with `dashboard-create`, capture its returned `id`, then attach every insight to it via `dashboards: []`: + +```json +{ + "name": "Analytics basics (wizard)", + "description": "Key views for the events instrumented by the PostHog wizard.", + "tags": ["wizard"] +} +``` + +When calling `insight-create`, use these known-good query shapes — they are verified against the MCP schema, and the common variations around them are rejected: + +A trends insight with a breakdown (breakdowns go in `breakdownFilter.breakdowns`, an array — there is NO top-level `breakdown` field on `TrendsQuery`): + +```json +{ + "name": "Signups by plan (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "TrendsQuery", + "series": [{ "kind": "EventsNode", "event": "user_signed_up", "math": "total" }], + "interval": "day", + "dateRange": { "date_from": "-30d" }, + "breakdownFilter": { "breakdowns": [{ "type": "event", "property": "plan" }] }, + "trendsFilter": { "display": "ActionsBar" } + } + } +} +``` + +A conversion funnel (the window fields are camelCase and live INSIDE `funnelsFilter` — not at the top level of `FunnelsQuery`, and not snake_case): + +```json +{ + "name": "Signup funnel (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "FunnelsQuery", + "series": [ + { "kind": "EventsNode", "event": "page_viewed" }, + { "kind": "EventsNode", "event": "user_signed_up" } + ], + "dateRange": { "date_from": "-30d" }, + "funnelsFilter": { + "funnelVizType": "steps", + "funnelOrderType": "ordered", + "funnelWindowInterval": 14, + "funnelWindowIntervalUnit": "day" + } + } + } +} +``` + +Valid `trendsFilter.display` values are `ActionsLineGraph`, `ActionsBar`, `ActionsAreaGraph`, `ActionsPie`, `ActionsStackedBar`, `BoldNumber`, and `ActionsTable` — names like `ActionsBarChart` or `ActionsBarGraph` are rejected. If an insight call is rejected anyway, fix the payload against these examples rather than retrying variations. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Compose the setup report as markdown — do NOT write it to a file in the project. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Then publish the report to the wizard session with a single `publish_handoff` call, passing the complete report markdown as `content`. This call is how the report reaches the user — do not write it to a file instead. + +Then mirror the report into a shareable PostHog notebook so the user has an in-app copy to link and comment on. Call `notebooks-create` with a `title` (e.g. `PostHog setup (wizard) – `) and `content` set to a single markdown node wrapping the report verbatim — `{"type":"doc","content":[{"type":"ph-markdown-notebook","attrs":{"nodeId":"markdown-notebook-v2","markdown":""}}]}`. Take the `short_id` from the response, build the notebook URL as `/project//notebooks/`, and emit it on its own line so the wizard can surface it: `[NOTEBOOK_URL]` followed by that URL. + +Upon completion, update `.posthog-events.json` so it matches the events you actually implemented, then remove it with your file tools. If removal is blocked or fails in your environment, leave the file in place and move on — the wizard host cleans it up after the run. Do not retry the removal or reach for shell commands to force it. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Published setup report to the wizard session +- Created notebook: [insert PostHog notebook URL] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-android/references/COMMANDMENTS.md b/skills/posthog/all/skills/integration-android/references/COMMANDMENTS.md new file mode 100644 index 00000000..c18de5d4 --- /dev/null +++ b/skills/posthog/all/skills/integration-android/references/COMMANDMENTS.md @@ -0,0 +1,9 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. diff --git a/skills/posthog/all/skills/integration-android/references/EXAMPLE.md b/skills/posthog/all/skills/integration-android/references/EXAMPLE.md index ab00c49d..32e7823d 100644 --- a/skills/posthog/all/skills/integration-android/references/EXAMPLE.md +++ b/skills/posthog/all/skills/integration-android/references/EXAMPLE.md @@ -1,7 +1,7 @@ # PostHog Android Example Project Repository: https://github.com/PostHog/context-mill -Path: basics/android +Path: example-apps/android --- diff --git a/skills/posthog/all/skills/integration-android/references/android.md b/skills/posthog/all/skills/integration-android/references/android.md index f618e21d..08a91f85 100644 --- a/skills/posthog/all/skills/integration-android/references/android.md +++ b/skills/posthog/all/skills/integration-android/references/android.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Android - Docs + +Copy page + # Android - Docs It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your mobile app. @@ -99,7 +105,7 @@ PostHog autocapture automatically tracks the following events for you: - **Application Installed** - when the app is installed. - **Application Updated** - when the app is updated. - **$screen** - when the user navigates. (if using `android.app.Activity`) -- **$exception** - when the app throws exceptions. +- **$exception** - when uncaught exception autocapture is enabled. To use this, enable [Android error tracking](/docs/error-tracking/installation/android.md) and exception autocapture in the SDK config. ### Capturing screen views @@ -178,6 +184,33 @@ You may find it helpful to get the current user's distinct ID. For example, to c To do this, call `distinctId()`. This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to `identify()`. +## Tracing headers + +Use `tracingHeaders` to connect Android network requests to backend events, errors, and LLM traces captured by a server-side PostHog SDK. Tracing headers are added by the `PostHogOkHttpInterceptor`, so install the interceptor on each `OkHttpClient` whose requests should include PostHog context. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHogOkHttpInterceptor +import com.posthog.android.PostHogAndroid +import com.posthog.android.PostHogAndroidConfig +import okhttp3.OkHttpClient +val config = PostHogAndroidConfig( + apiKey = POSTHOG_API_KEY, + host = POSTHOG_HOST, +).apply { + tracingHeaders = listOf("api.example.com") +} +PostHogAndroid.setup(this, config) +val okHttpClient = OkHttpClient.Builder() + .addInterceptor(PostHogOkHttpInterceptor()) + .build() +``` + +Hostnames are matched exactly and should not include protocols, paths, ports, or wildcard subdomains. Matching OkHttp requests include `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` when those values are available. + ## Alias Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. @@ -348,8 +381,8 @@ PostHog AI ```kotlin val config = PostHogAndroidConfig( - apiKey = , - host = https://us.i.posthog.com + apiKey = "", + host = "https://us.i.posthog.com" ) config.optOut = true PostHogAndroid.setup(this, config) @@ -387,9 +420,9 @@ PostHog.isOptOut() ## Flush -You can set the number of events in the configuration that should queue before flushing. Setting this to `1` will send events immediately and will use more battery. The default value for this is `20`. +You can configure how many events queue before flushing with `flushAt`. Setting this to `1` will send events immediately and will use more battery. The default is `20`. -You can also configure the flush interval. By default we flush all events after `30` seconds, no matter how many events have been gathered. +You can also configure the flush interval with `flushIntervalSeconds` (default `30`), after which queued events are sent regardless of how many have been gathered: Kotlin @@ -403,7 +436,7 @@ val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST) } ``` -You can also manually flush the queue: +You can also manually flush the queue to start sending events immediately instead of waiting for the next batch: Kotlin @@ -414,6 +447,8 @@ import com.posthog.PostHog PostHog.flush() ``` +Flushing is best-effort and asynchronous – it starts sending queued events in the background but doesn't wait for the request to finish, so it isn't a delivery guarantee. + ## Reset after logout To reset the user's ID and anonymous ID, call `reset`. Usually you would do this right after the user logs out. @@ -439,10 +474,11 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.isFeatureEnabled("flag-key")) { +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.enabled == true) { // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload } ``` @@ -454,10 +490,26 @@ PostHog AI ```kotlin import com.posthog.PostHog -if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant +val result = PostHog.getFeatureFlagResult("flag-key") +if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant // Do something differently for this user - // Optional: fetch the payload - val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key") + // Optional: fetch the payload from the same evaluation result + val matchedFlagPayload = result.payload +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `PostHog.getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.getAllFeatureFlags()?.forEach { flag -> + println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}") } ``` @@ -485,7 +537,7 @@ val config = PostHogAndroidConfig(apiKey = "").apply { } } } -// And/Or manually the SDK is initialized +// And/or after the SDK is initialized PostHog.reloadFeatureFlags { if (PostHog.isFeatureEnabled("flag-key")) { // do something @@ -506,9 +558,58 @@ import com.posthog.PostHog PostHog.reloadFeatureFlags() ``` +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHog +PostHog.captureFeatureView("flag-key", flagVariant = "variant-key") +PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key") +``` + +### Bootstrapping flags + +Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. + +To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones. + +Set `config.bootstrap` before calling `setup()` to seed identity and flag values before the first `/flags` response (requires Android SDK `3.55.0`+): + +Kotlin + +PostHog AI + +```kotlin +import com.posthog.PostHogBootstrapConfig +val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST) +config.bootstrap = PostHogBootstrapConfig( + distinctId = "distinct_id_of_your_user", + isIdentifiedId = true, + featureFlags = mapOf( + "flag-1" to true, + "variant-flag" to "control" + ) +) +PostHogAndroid.setup(this, config) +``` + +- **Bootstrapped identity applies during setup.** On a fresh install, setting it before `setup()` means events captured synchronously during initialization (like `Application Installed`) carry your distinct ID instead of the SDK-generated UUID. + - An **anonymous** bootstrap (`isIdentifiedId: false`, the default) seeds the anonymous ID only when none is persisted yet. Once an anonymous ID exists on disk, or the person has been identified, the SDK ignores it. + - An **identified** bootstrap (`isIdentifiedId: true`) is for a signed-in identity available to your app (for example, from a backend session token). On a fresh install, it seeds the distinct ID, marks the person identified, and generates a separate device ID. On a returning install, a matching anonymous ID is marked identified without emitting `$identify`; a different anonymous ID is merged via `identify()` when person profiles are enabled. This emits `$identify` unless capturing is opted out. A different, already-identified person is left untouched. +- **Bootstrapped flags are served until the first `/flags` response, then replaced.** A complete `/flags` response takes over entirely, so bootstrapped-only keys don't persist past it. Only *enabled* flags are seeded: a `true` boolean or a non-empty variant string. A `false` or empty value is dropped, matching posthog-js. Seed payloads with the separate `featureFlagPayloads` option. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared on `reset()`. + +The feature-flags-loaded signal fires as soon as bootstrapped flags are applied, so startup logic can read them immediately. These SDKs don't support the `sessionID` bootstrap option. When person profiles are set to `never`, the SDK preserves a different anonymous identity instead of merging it into an identified bootstrap. + +See the [SDK bootstrapping guide](/docs/libraries/bootstrapping.md) for the cross-SDK overview. + ## Experiments (A/B tests) -Since [experiments](/docs/experiments/manual.md) use feature flags, the code for running an experiment is very similar to the feature flags code: +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: Kotlin @@ -563,12 +664,22 @@ The `name` is a special property which is used in the PostHog UI for the name of ## Error tracking -To set up error tracking in your project, follow the [Android installation guide](/docs/error-tracking/installation/android.md). +To set up error tracking in your project, see the [error tracking docs](/docs/error-tracking.md). + +## Logs + +To set up [logs](/docs/logs.md) in your Android app, follow the [Android logs installation guide](/docs/logs/installation/android.md). The SDK exposes `PostHog.logger.{trace,debug,info,warn,error,fatal}` for sending structured records to PostHog Logs, with batching, offline persistence, and a rate cap built in. + +> **Minimum version:** `com.posthog:posthog-android@3.46.0` or later. ## Session replay To set up [session replay](/docs/session-replay/mobile.md) in your project, all you need to do is install the Android SDK, enable "Record user sessions" in [your project settings](https://us.posthog.com/settings/project-replay) and enable the `sessionReplay` option. +## Surveys + +To set up surveys, follow the [additional installation instructions for Android](/docs/surveys/installation/android.md). Surveys launched with [popover presentation](/docs/surveys/creating-surveys.md#presentation) are automatically shown to users matching the [display conditions](/docs/surveys/creating-surveys.md#display-conditions) you set up. + ## Offline behavior The PostHog Android SDK will continue to capture events when the device is offline. The events are stored in a queue in the device's file storage and are flushed when the device is online. @@ -580,7 +691,7 @@ The PostHog Android SDK will continue to capture events when the device is offli ## Debug mode -If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. +If you're not seeing the expected events being captured, the feature flags being evaluated, surveys being shown, or session replay/error tracking behavior, you can enable debug mode to see what's happening. You can enable debug mode by setting the `debug` option to `true` in the `PostHogAndroidConfig` object. This will enable verbose logs about the inner workings of the SDK. @@ -597,76 +708,149 @@ val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST) ## All configuration options -When creating the PostHog client, there are many options you can set: +When creating the PostHog client, pass a `PostHogAndroidConfig`. It inherits the core `PostHogConfig` options and adds Android-specific options. Kotlin PostHog AI ```kotlin -val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST).apply { - // Capture certain application events automatically. (on/true by default) +import com.posthog.PersonProfiles +import com.posthog.android.PostHogAndroidConfig +val config = PostHogAndroidConfig( + apiKey = POSTHOG_API_KEY, + host = POSTHOG_HOST +).apply { captureApplicationLifecycleEvents = true - // Capture screen views automatically. (on/true by default) - captureScreenViews = true // (on/true by default) - // Capture deep links as part of the screen call. (on/true by default) + captureScreenViews = true captureDeepLinks = true - // Maximum number of events to keep in queue before flushing (20 by default) flushAt = 20 - // Number of maximum events in memory and disk, when the maximum is exceed, the oldest event is deleted and the new one takes place. (1000 by default) maxQueueSize = 1000 - // Number of maximum events in a batch call. (50 by default) maxBatchSize = 50 - // Maximum delay before flushing the queue (30 seconds) + maxRetries = 3 flushIntervalSeconds = 30 - // Logs the SDK messages into Logcat. (off/false by default) debug = false - // Prevents capturing any data if enabled. (off/false by default) optOut = false - // Send a '$feature_flag_called' event when a feature flag is used automatically. (on/true by default) sendFeatureFlagEvent = true - // Preload feature flags automatically. (on/true by default) + featureFlagCalledCacheSize = 1000 preloadFeatureFlags = true - // Evaluation context tags that constrain which feature flags are evaluated. (not set by default) - // When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. - // Available in version 3.25.0+. The legacy parameter `evaluationEnvironments` (version 3.24.0+) is also supported. evaluationContexts = listOf("production", "android", "mobile") - // Callback that is called when feature flags are loaded (not set by default) - onFeatureFlags = { ... } - // Callback that allows to sanitize the event properties (not set by default) - propertiesSanitizer = { properties -> ... } - // Hook for encrypt and decrypt events - // Devices are sandbox already - // Defaults to no encryption - encryption = object : PostHogEncryption { ... } - // Hook that allows for modification of the default mechanism for - // generating anonymous id (which as of now is just random UUID v7) - getAnonymousId = { ... } - // Determines the behavior for processing user profiles. - // Defaults to PersonProfiles.IDENTIFIED_ONLY + setDefaultPersonProperties = true personProfiles = PersonProfiles.IDENTIFIED_ONLY - // Enable Recording of Session Replay. (off/false by default) - sessionReplay = false - // Session Replay configuration - // https://posthog.com/docs/session-replay/installation for more details - sessionReplayConfig = PostHogSessionReplayConfig(...) - // Whether the SDK should reuse the anonymous Id between user changes. - // When enabled, a single Id will be used for all anonymous users on this device (off/false by default) reuseAnonymousId = false - // Error tracking configuration - errorTrackingConfig = PostHogErrorTrackingConfig(...) + sessionReplay = false + errorTrackingConfig.autoCapture = false +} +``` + +### Android-specific options + +| Option | Default | Description | +| --- | --- | --- | +| captureApplicationLifecycleEvents | true | Captures Application Installed, Application Updated, Application Opened, and Application Backgrounded. | +| captureScreenViews | true | Captures $screen for foreground android.app.Activity screens. | +| captureDeepLinks | true | Captures Deep Link Opened with URL/query/referrer properties. | + +### Core options + +| Option | Default | Description | +| --- | --- | --- | +| debug | false | Enables verbose SDK logs in Logcat. You can also call PostHog.debug(true). | +| optOut | false | Prevents data capture when enabled. You can also call PostHog.optOut() and PostHog.optIn(). | +| flushAt | 20 | Number of queued events that triggers a flush. | +| maxQueueSize | 1000 | Maximum number of events kept across memory and disk before FIFO eviction. | +| maxBatchSize | 50 | Maximum number of events sent in one batch request. | +| maxRetries | 3 | Maximum retry attempts for failed requests. | +| flushIntervalSeconds | 30 | Maximum delay before queued data is flushed. | +| encryption | null | Optional PostHogEncryption implementation for encrypting persisted queued events. | +| proxy | null | Optional java.net.Proxy for PostHog API requests. | +| getAnonymousId | generated UUID | Optional hook to customize anonymous ID generation. | +| reuseAnonymousId | false | Reuses one anonymous ID across user changes on the same device. | +| personProfiles | PersonProfiles.IDENTIFIED_ONLY | Controls when person profiles are processed: IDENTIFIED_ONLY, ALWAYS, or NEVER. | +| setDefaultPersonProperties | true | Includes default device and app properties in feature flag evaluation requests. | +| releaseIdentifier | app/version fallback | Release identifier used by error tracking and uploaded ProGuard/R8 mappings. The Android Gradle plugin can inject this automatically. | +| tracingHeaders | null | Exact hostnames that should receive PostHog tracing headers when using PostHogOkHttpInterceptor. | + +### Feature flag options + +| Option | Default | Description | +| --- | --- | --- | +| sendFeatureFlagEvent | true | Sends $feature_flag_called when a feature flag is evaluated. | +| featureFlagCalledCacheSize | 1000 | Number of feature flag calls cached for deduplicating $feature_flag_called events. | +| preloadFeatureFlags | true | Fetches feature flags automatically during setup. | +| evaluationContexts | null | Context tags that constrain which feature flags are evaluated. Available in version 3.29.1+. The legacy evaluationEnvironments option is available in version 3.24.0+. | +| onFeatureFlags | null | Callback invoked when feature flags are loaded. | + +### Product configuration objects + +| Option | Default | Description | +| --- | --- | --- | +| sessionReplay | false | Enables session replay when project settings also allow recording. | +| sessionReplayConfig | PostHogSessionReplayConfig() | Configures masking, screenshots, Logcat capture, sampling, and custom drawable conversion. | +| logs | PostHogLogsConfig() | Configures [Android logs](/docs/logs/installation/android.md). | +| errorTrackingConfig | PostHogErrorTrackingConfig() | Configures error tracking. autoCapture defaults to false; set it to true to autocapture uncaught exceptions when project settings also enable error tracking. | +| surveys | false | Internal/experimental native Android survey support. Native Android survey UI is not fully supported or documented yet. | +| surveysConfig | PostHogSurveysConfig() | Internal/experimental survey display delegate configuration, primarily for hybrid SDKs. | +| bootstrap | null | Seeds identity (distinctId, isIdentifiedId) and feature-flag state (featureFlags, featureFlagPayloads) before the first /flags response. Bootstrapped identity applies to the first session; only enabled flags are served, until the first /flags response replaces them. See [SDK bootstrapping](/docs/libraries/bootstrapping.md#behavior-on-mobile-sdks). | + +### Event filtering with `beforeSend` + +Use `addBeforeSend` to redact, modify, or drop events before they are queued. Return `null` to drop an event. + +Kotlin + +PostHog AI + +```kotlin +config.addBeforeSend { event -> + event.properties?.remove("password") + if (event.event == "internal_debug_event") { + null + } else { + event + } +} +``` + +#### Filtering autocaptured screens + +You can stop specific screens from being autocaptured by filtering them in your before-send hook. Return `null` for any `$screen` event whose `$screen_name` matches a screen you don't want to track, and it's dropped before being sent – keeping unwanted screen views out of your event log. + +Because it's just a function, you can filter however you like – an **ignorelist** (drop the screens you name), an **allowlist** (invert the check to capture only the screens you name), or any custom rule such as a name prefix, a regex, or a check against the event's properties. + +Kotlin + +PostHog AI + +```kotlin +val ignoredScreens = setOf("Splash", "Debug") +config.addBeforeSend { event -> + val screenName = event.properties?.get("$screen_name") as? String + if (event.event == "$screen" && screenName in ignoredScreens) { + null + } else { + event + } } ``` +## Push notifications + +The Android SDK can register a device for [Workflows](/docs/workflows.md) push notifications and capture when a user opens one. For setup, including automatic and manual registration, capturing opens, opting out, and identity verification, see [Push notifications](/docs/workflows/push-notifications.md). + ## FAQ +## What Android API level is required? + +The Android SDK supports Android API 23 and newer. + ## Do I need to declare permissions in the AndroidManifest.xml? -We don't declare nor use any 'Service', so no permissions are needed. +Usually, no. The SDK declares `android.permission.INTERNET` and `android.permission.ACCESS_NETWORK_STATE`, and Android's manifest merger adds them to your app. The SDK does not declare or require an Android `Service`. -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/integration-android/references/identify-users.md b/skills/posthog/all/skills/integration-android/references/identify-users.md index c27226ee..8647dcb3 100644 --- a/skills/posthog/all/skills/integration-android/references/identify-users.md +++ b/skills/posthog/all/skills/integration-android/references/identify-users.md @@ -1,10 +1,16 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + # Identify users - Docs Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. -However, in the frontend of a [web](/docs/libraries/js/features.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/features.md#capturing-anonymous-events). +However, in the frontend of a [web](/docs/libraries/js/usage.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/usage.md#capturing-anonymous-events). To link events to specific users, call `identify`: @@ -54,9 +60,10 @@ posthog.identify('distinct_id', { // Replace "distinct_id" with your user's uniq await Posthog().identify( userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier userProperties: { - email: "max@hedgehogmail.com", // optional: set additional person properties - name: "Max Hedgehog" -}); + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); ``` Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. @@ -93,6 +100,31 @@ You only need to call `identify` once per session, and you should avoid calling If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + ### 2\. Use unique strings for distinct IDs If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: @@ -141,7 +173,7 @@ posthog.reset() ### Dart ```dart -Posthog().reset() +await Posthog().reset(); ``` If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: @@ -164,6 +196,10 @@ Whenever possible, we recommend passing in all person properties you have availa Person properties can also be set being adding a `$set` property to a event `capture` call. +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. ### 5\. Use deep links between platforms @@ -182,20 +218,89 @@ In these cases, you can use a [deep link](https://developer.android.com/training 2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. 3. When the user is redirected to the app, parse the deep link and handle the following cases: -- The user is already authenticated on the mobile app. In this case, call [`posthog.alias()`](/docs/libraries/js/features.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. -- The user is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/features.md#identifying-users) with the distinct ID from the web. Events will be associated with this distinct ID. +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/usage.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/usage.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + ## Further reading - [Identifying users docs](/docs/product-analytics/identify.md) - [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) - [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/integration-angular/SKILL.md b/skills/posthog/all/skills/integration-angular/SKILL.md index f6f200ce..cab97da6 100644 --- a/skills/posthog/all/skills/integration-angular/SKILL.md +++ b/skills/posthog/all/skills/integration-angular/SKILL.md @@ -3,7 +3,7 @@ name: integration-angular description: PostHog integration for Angular applications metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog integration for Angular @@ -14,20 +14,21 @@ This skill helps you add PostHog analytics to Angular applications. Follow these steps in order to complete the integration: -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion ## Reference files - `references/EXAMPLE.md` - Angular example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation - `references/angular.md` - Angular - docs - `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow The example project shows the target implementation pattern. Consult the documentation for API details. @@ -39,10 +40,25 @@ The example project shows the target implementation pattern. Consult the documen ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. - Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. - Always use standalone components over NgModules. - Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). ## Identifying users diff --git a/skills/posthog/all/skills/integration-angular/references/1-begin.md b/skills/posthog/all/skills/integration-angular/references/1-begin.md new file mode 100644 index 00000000..55f0a832 --- /dev/null +++ b/skills/posthog/all/skills/integration-angular/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-angular/references/2-edit.md b/skills/posthog/all/skills/integration-angular/references/2-edit.md new file mode 100644 index 00000000..e5f7ffd1 --- /dev/null +++ b/skills/posthog/all/skills/integration-angular/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-angular/references/3-revise.md b/skills/posthog/all/skills/integration-angular/references/3-revise.md new file mode 100644 index 00000000..3b07f506 --- /dev/null +++ b/skills/posthog/all/skills/integration-angular/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-angular/references/4-conclude.md b/skills/posthog/all/skills/integration-angular/references/4-conclude.md new file mode 100644 index 00000000..1523281b --- /dev/null +++ b/skills/posthog/all/skills/integration-angular/references/4-conclude.md @@ -0,0 +1,143 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Create a live PostHog dashboard named "Analytics basics (wizard)" from the events you just instrumented, then populate it with up to five insights — lead with the business-critical views: conversion funnels, churn events, and other key signals. Use the exact same event names as implemented in the code. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. + +Always create the dashboard and insights based on the intended captures, regardless of whether those events have been observed yet. An insight is a definition over event names, not a snapshot of current data: it is expected to render empty until the first events arrive, and it fills in on its own once they do. "No data ingested yet", "the events aren't in the schema", or "the query would return nothing today" are never reasons to skip or defer insights — a dashboard handed off without them is an incomplete integration, not a cautious one. + +## How to call PostHog MCP tools + +The PostHog MCP server exposes a single `exec` tool. Every PostHog operation is driven by a CLI-style command string passed in its `command` parameter — the tool may be namespaced by the host (`mcp__posthog__exec`, `mcp__posthog-wizard__exec`), but the command grammar is the same. Tool names and schemas are not predictable, so discover and inspect before you call. + +**Grammar** — run in this order: + +```text +exec({ "command": "search " }) # find tools by name/title/description; `tools` lists them all +exec({ "command": "info " }) # REQUIRED before every call — description + input schema +exec({ "command": "schema " }) # drill into a field the schema flags with a `hint` +exec({ "command": "call " }) # run the tool +``` + +Running `info ` before `call ` is mandatory, the same way you read a file before editing it. `info` returns the full schema for simple tools; for large ones it summarizes and attaches `hint` entries pointing at fields to drill into with `schema`. Dot-notation descends objects (`query.source`), array items (`series.0.properties`), and unions. Never guess the structure of a field that carries a hint — drill first. + +Every PostHog tool goes through `exec` this way — there is no separate named tool to call directly. The inner tool names and JSON payloads below are what you pass to `call`. + +**Errors** carry a suggestion and similar tool names — read it before retrying. If a name isn't found it may have been renamed; run `search ` or `tools` again to find the current one. + +Create the parent dashboard first with `dashboard-create`, capture its returned `id`, then attach every insight to it via `dashboards: []`: + +```json +{ + "name": "Analytics basics (wizard)", + "description": "Key views for the events instrumented by the PostHog wizard.", + "tags": ["wizard"] +} +``` + +When calling `insight-create`, use these known-good query shapes — they are verified against the MCP schema, and the common variations around them are rejected: + +A trends insight with a breakdown (breakdowns go in `breakdownFilter.breakdowns`, an array — there is NO top-level `breakdown` field on `TrendsQuery`): + +```json +{ + "name": "Signups by plan (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "TrendsQuery", + "series": [{ "kind": "EventsNode", "event": "user_signed_up", "math": "total" }], + "interval": "day", + "dateRange": { "date_from": "-30d" }, + "breakdownFilter": { "breakdowns": [{ "type": "event", "property": "plan" }] }, + "trendsFilter": { "display": "ActionsBar" } + } + } +} +``` + +A conversion funnel (the window fields are camelCase and live INSIDE `funnelsFilter` — not at the top level of `FunnelsQuery`, and not snake_case): + +```json +{ + "name": "Signup funnel (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "FunnelsQuery", + "series": [ + { "kind": "EventsNode", "event": "page_viewed" }, + { "kind": "EventsNode", "event": "user_signed_up" } + ], + "dateRange": { "date_from": "-30d" }, + "funnelsFilter": { + "funnelVizType": "steps", + "funnelOrderType": "ordered", + "funnelWindowInterval": 14, + "funnelWindowIntervalUnit": "day" + } + } + } +} +``` + +Valid `trendsFilter.display` values are `ActionsLineGraph`, `ActionsBar`, `ActionsAreaGraph`, `ActionsPie`, `ActionsStackedBar`, `BoldNumber`, and `ActionsTable` — names like `ActionsBarChart` or `ActionsBarGraph` are rejected. If an insight call is rejected anyway, fix the payload against these examples rather than retrying variations. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Compose the setup report as markdown — do NOT write it to a file in the project. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Then publish the report to the wizard session with a single `publish_handoff` call, passing the complete report markdown as `content`. This call is how the report reaches the user — do not write it to a file instead. + +Then mirror the report into a shareable PostHog notebook so the user has an in-app copy to link and comment on. Call `notebooks-create` with a `title` (e.g. `PostHog setup (wizard) – `) and `content` set to a single markdown node wrapping the report verbatim — `{"type":"doc","content":[{"type":"ph-markdown-notebook","attrs":{"nodeId":"markdown-notebook-v2","markdown":""}}]}`. Take the `short_id` from the response, build the notebook URL as `/project//notebooks/`, and emit it on its own line so the wizard can surface it: `[NOTEBOOK_URL]` followed by that URL. + +Upon completion, update `.posthog-events.json` so it matches the events you actually implemented, then remove it with your file tools. If removal is blocked or fails in your environment, leave the file in place and move on — the wizard host cleans it up after the run. Do not retry the removal or reach for shell commands to force it. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Published setup report to the wizard session +- Created notebook: [insert PostHog notebook URL] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-angular/references/COMMANDMENTS.md b/skills/posthog/all/skills/integration-angular/references/COMMANDMENTS.md new file mode 100644 index 00000000..5fb708c1 --- /dev/null +++ b/skills/posthog/all/skills/integration-angular/references/COMMANDMENTS.md @@ -0,0 +1,23 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Use inject() instead of constructor injection. PostHog service should be injected via inject() in components/services that need it. +- Create a dedicated PosthogService as a singleton root service that wraps the PostHog SDK. +- Always use standalone components over NgModules. +- Configure PostHog credentials in src/environments/environment.ts files, as Angular reads environment variables from these configuration files +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). diff --git a/skills/posthog/all/skills/integration-angular/references/EXAMPLE.md b/skills/posthog/all/skills/integration-angular/references/EXAMPLE.md index b1856328..3aa813eb 100644 --- a/skills/posthog/all/skills/integration-angular/references/EXAMPLE.md +++ b/skills/posthog/all/skills/integration-angular/references/EXAMPLE.md @@ -1,7 +1,7 @@ # PostHog Angular Example Project Repository: https://github.com/PostHog/context-mill -Path: basics/angular +Path: example-apps/angular --- @@ -155,11 +155,7 @@ posthogService.posthog.capture('burrito_considered', { ### Error tracking (pages/profile/profile.component.ts) ```typescript -posthogService.posthog.capture('$exception', { - $exception_message: error.message, - $exception_type: error.name, - $exception_stack_trace_raw: error.stack, -}); +posthogService.posthog.captureException(error); ``` ## Angular-specific details @@ -514,7 +510,7 @@ import { AuthService } from '../../services/auth.service'; @if (auth.user(); as user) {

Welcome back, {{ user.username }}!

-

You are now logged in. Feel free to explore:

+

You are logged in. Feel free to explore:

  • Consider the potential of burritos
  • View your profile and statistics
  • @@ -678,11 +674,7 @@ export class ProfileComponent { throw new Error('Test error for PostHog error tracking'); } catch (err) { const error = err as Error; - this.posthogService.posthog.capture('$exception', { - $exception_message: error.message, - $exception_type: error.name, - $exception_stack_trace_raw: error.stack, - }); + this.posthogService.posthog.captureException(error); console.error('Captured error:', err); alert('Error captured and sent to PostHog!'); } diff --git a/skills/posthog/all/skills/integration-angular/references/angular.md b/skills/posthog/all/skills/integration-angular/references/angular.md index 9177b55c..91c75a83 100644 --- a/skills/posthog/all/skills/integration-angular/references/angular.md +++ b/skills/posthog/all/skills/integration-angular/references/angular.md @@ -1,3 +1,9 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Angular - Docs + +Copy page + # Angular - Docs PostHog makes it easy to get data about traffic and usage of your [Angular](https://angular.dev/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more. @@ -34,6 +40,18 @@ pnpm add posthog-js bun add posthog-js ``` +> **If your site sets a Content-Security-Policy**, it needs to allow PostHog. This applies to the snippet and to package installs alike: the SDK lazy-loads extra bundles (session replay, surveys) from PostHog's CDN, and sends events to the ingestion host. PostHog serves from subdomains of `posthog.com` that change over time, so allow the wildcard: +> +> PostHog AI +> +> ``` +> script-src 'self' https://*.posthog.com; +> connect-src 'self' https://*.posthog.com; +> worker-src 'self' blob: data:; +> ``` +> +> `script-src` covers the snippet and the lazy-loaded bundles, `connect-src` covers event ingestion and feature flags, and `worker-src` covers session replay. The [toolbar needs a few more](/docs/advanced/content-security-policy.md), or use a [reverse proxy](/docs/advanced/proxy.md) so everything is first-party. Failing to do so causes silent failures where `capture` and `identify` calls never send, so the integration looks complete while zero events arrive. Remember `connect-src` falls back to `default-src`, so `default-src 'self'` blocks event delivery even when the script itself is bundled. + ### Initialize the PostHog client Generate environment files for your project with `ng g environments`. Configure the following environment variables: @@ -53,16 +71,13 @@ PostHog AI ```typescript // src/app/services/posthog.service.ts -import { DestroyRef, Injectable, NgZone } from "@angular/core"; +import { Injectable, NgZone } from "@angular/core"; import posthog from "posthog-js"; import { environment } from "../../environments/environment"; -import { Router } from "@angular/router"; @Injectable({ providedIn: "root" }) export class PosthogService { constructor( private ngZone: NgZone, - private router: Router, - private destroyRef: DestroyRef, ) { this.initPostHog(); } @@ -70,7 +85,7 @@ export class PosthogService { this.ngZone.runOutsideAngular(() => { posthog.init(environment.posthogKey, { api_host: environment.posthogHost, - defaults: '2026-01-30', + defaults: '2026-05-30', }); }); } @@ -120,7 +135,7 @@ import { environment } from "./environments/environment"; import posthog from 'posthog-js' posthog.init(environment.posthogKey, { api_host: environment.posthogHost, - defaults: '2026-01-30' + defaults: '2026-05-30' }) bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); @@ -130,8 +145,30 @@ bootstrapApplication(AppComponent, appConfig) > **Identifying users is required.** Call `posthog.identify('your-user-id')` after login to link events to a known user. This is what connects frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), and [error tracking](/docs/error-tracking.md) to the same person — and lets backend events link back too. > +> Use a stable ID from your auth system when possible, not an email or display name. Send those as person properties instead. If your app has no other stable key, email works as a fallback if they are unique. Never a shared literal like `"anonymous"` or `"user"`, which pools many people onto one person and corrupts their data. When no ID is available at all, skip the identify and retain the anonymous distinct ID that's automatically assigned. +> +> Call `posthog.reset()` on logout, so the next person to use the browser doesn't inherit the last one's identity. +> > See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. +If your app calls your own backend, `tracing_headers` adds `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` to matching `fetch` and `XMLHttpRequest` requests. This lets server-side SDKs link backend events, errors, and LLM traces back to frontend sessions and replays. Use hostnames only, without protocols or paths. + +JavaScript + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + // Optional: send PostHog session/user context to your backend + tracing_headers: ['api.example.com'], +}) +``` + +This works in local development too, but match on the hostname alone: use `'localhost'`, not `'localhost:3000'`. Ports are never part of a hostname, so a value with one in it never matches anything. `localhost` and `127.0.0.1` are also different hostnames — use whichever your app actually calls. + +Tracing headers help you attribute events across front and backend consistently. When this isn't available, use your server-side stable IDs to deduce the matching `distinctId`, and pass it in when capturing the event. + > **Note:** If you're using Typescript, you might have some trouble getting your types to compile because we depend on `rrweb` but don't ship all of their types. To accommodate that, you'll need to add `@rrweb/types@2.0.0-alpha.17` and `rrweb-snapshot@2.0.0-alpha.17` as a dependency if you want your Angular compiler to typecheck correctly. > > Given the nature of this library, you might need to completely clear your `.npm` cache to get this to work as expected. Make sure your clear your CI's cache as well. @@ -154,7 +191,7 @@ This makes it possible to track users across their entire journey (e.g. from vis Add IPs to Firewall/WAF allowlists (recommended) -For certain features like [heatmaps](/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site. +For certain features like [heatmaps](/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog's requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site. **EU**: `3.75.65.221`, `18.197.246.42`, `3.120.223.253` @@ -234,22 +271,21 @@ posthog.service.ts PostHog AI ```typescript -import { isPlatformBrowser } from "@angular/common"; -import { PLATFORM_ID } from "@angular/core"; -import { isPlatformBrowser } from "@angular/common"; import { PLATFORM_ID } from "@angular/core"; @Injectable({ providedIn: "root" }) export class PosthogService { constructor( private ngZone: NgZone, - private router: Router, - private destroyRef: DestroyRef, @Inject(PLATFORM_ID) private platformId: Object ) { // Only initialize PostHog in browser environment if (isPlatformBrowser(this.platformId)) { this.initPostHog(); //+ } + } + private initPostHog() { + this.ngZone.runOutsideAngular(() => { + posthog.init(environment.posthogKey, { ``` ### 2\. Add server-side initialization @@ -319,11 +355,9 @@ app.get('**', async (req, res, next) => { const distinctId = getDistinctIdFromCookie(headers.cookie); let isFeatureEnabled = false; const client = new PostHog( - const client = new PostHog( environment.posthogKey, { host: environment.posthogHost } - ) - ) + ); if (distinctId) { client.capture({ distinctId: distinctId, @@ -365,7 +399,7 @@ Angular SSR does not allow Node.js code to be bundled into client-side component ## Next steps -For any technical questions for how to integrate specific PostHog features into Angular (such as feature flags, A/B testing, surveys, etc.), have a look at our [JavaScript Web SDK docs](/docs/libraries/js/features.md). +For any technical questions for how to integrate specific PostHog features into Angular (such as feature flags, A/B testing, surveys, etc.), have a look at our [JavaScript Web SDK docs](/docs/libraries/js/usage.md). Alternatively, the following tutorials can help you get started: @@ -373,9 +407,9 @@ Alternatively, the following tutorials can help you get started: - [How to set up A/B tests in Angular](/tutorials/angular-ab-tests.md) - [How to set up surveys in Angular](/tutorials/angular-surveys.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/integration-angular/references/identify-users.md b/skills/posthog/all/skills/integration-angular/references/identify-users.md index c27226ee..8647dcb3 100644 --- a/skills/posthog/all/skills/integration-angular/references/identify-users.md +++ b/skills/posthog/all/skills/integration-angular/references/identify-users.md @@ -1,10 +1,16 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + # Identify users - Docs Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. -However, in the frontend of a [web](/docs/libraries/js/features.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/features.md#capturing-anonymous-events). +However, in the frontend of a [web](/docs/libraries/js/usage.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/usage.md#capturing-anonymous-events). To link events to specific users, call `identify`: @@ -54,9 +60,10 @@ posthog.identify('distinct_id', { // Replace "distinct_id" with your user's uniq await Posthog().identify( userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier userProperties: { - email: "max@hedgehogmail.com", // optional: set additional person properties - name: "Max Hedgehog" -}); + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); ``` Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. @@ -93,6 +100,31 @@ You only need to call `identify` once per session, and you should avoid calling If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + ### 2\. Use unique strings for distinct IDs If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: @@ -141,7 +173,7 @@ posthog.reset() ### Dart ```dart -Posthog().reset() +await Posthog().reset(); ``` If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: @@ -164,6 +196,10 @@ Whenever possible, we recommend passing in all person properties you have availa Person properties can also be set being adding a `$set` property to a event `capture` call. +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. ### 5\. Use deep links between platforms @@ -182,20 +218,89 @@ In these cases, you can use a [deep link](https://developer.android.com/training 2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. 3. When the user is redirected to the app, parse the deep link and handle the following cases: -- The user is already authenticated on the mobile app. In this case, call [`posthog.alias()`](/docs/libraries/js/features.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. -- The user is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/features.md#identifying-users) with the distinct ID from the web. Events will be associated with this distinct ID. +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/usage.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/usage.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + ## Further reading - [Identifying users docs](/docs/product-analytics/identify.md) - [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) - [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) -### Community questions +### Still have questions? -Ask a question +Ask PostHog AI ### Was this page useful? diff --git a/skills/posthog/all/skills/integration-astro-hybrid/SKILL.md b/skills/posthog/all/skills/integration-astro-hybrid/SKILL.md index fffaa2ce..0f33fd23 100644 --- a/skills/posthog/all/skills/integration-astro-hybrid/SKILL.md +++ b/skills/posthog/all/skills/integration-astro-hybrid/SKILL.md @@ -5,7 +5,7 @@ description: >- server-rendered pages metadata: author: PostHog - version: 1.9.4 + version: dev --- # PostHog integration for Astro (Hybrid) @@ -16,20 +16,21 @@ This skill helps you add PostHog analytics to Astro (Hybrid) applications. Follow these steps in order to complete the integration: -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion ## Reference files - `references/EXAMPLE.md` - Astro (Hybrid) example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation - `references/astro.md` - Astro - docs - `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion +- `references/COMMANDMENTS.md` - Framework-specific rules the integration must follow The example project shows the target implementation pattern. Consult the documentation for API details. @@ -41,15 +42,39 @@ The example project shows the target implementation pattern. Consult the documen ## Framework guidelines +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op - Always use the is:inline directive on PostHog script tags to prevent Astro from processing them and causing TypeScript errors - Use PUBLIC_ prefix for client-side environment variables in Astro (e.g., PUBLIC_POSTHOG_PROJECT_TOKEN) - Create a posthog.astro component in src/components/ for reusable initialization across pages - Import the PostHog component in a Layout and wrap all pages with that layout - 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 +- Set tracing_headers to the backend hostname on the client (hostnames only, no port) so posthog-js sends X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID on every request, and read both server-side so its events attribute to the same person - 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead +- 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')) +- 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 ## Identifying users diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/1-begin.md b/skills/posthog/all/skills/integration-astro-hybrid/references/1-begin.md new file mode 100644 index 00000000..55f0a832 --- /dev/null +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/2-edit.md b/skills/posthog/all/skills/integration-astro-hybrid/references/2-edit.md new file mode 100644 index 00000000..e5f7ffd1 --- /dev/null +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/3-revise.md b/skills/posthog/all/skills/integration-astro-hybrid/references/3-revise.md new file mode 100644 index 00000000..3b07f506 --- /dev/null +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/4-conclude.md b/skills/posthog/all/skills/integration-astro-hybrid/references/4-conclude.md new file mode 100644 index 00000000..1523281b --- /dev/null +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/4-conclude.md @@ -0,0 +1,143 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Create a live PostHog dashboard named "Analytics basics (wizard)" from the events you just instrumented, then populate it with up to five insights — lead with the business-critical views: conversion funnels, churn events, and other key signals. Use the exact same event names as implemented in the code. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. + +Always create the dashboard and insights based on the intended captures, regardless of whether those events have been observed yet. An insight is a definition over event names, not a snapshot of current data: it is expected to render empty until the first events arrive, and it fills in on its own once they do. "No data ingested yet", "the events aren't in the schema", or "the query would return nothing today" are never reasons to skip or defer insights — a dashboard handed off without them is an incomplete integration, not a cautious one. + +## How to call PostHog MCP tools + +The PostHog MCP server exposes a single `exec` tool. Every PostHog operation is driven by a CLI-style command string passed in its `command` parameter — the tool may be namespaced by the host (`mcp__posthog__exec`, `mcp__posthog-wizard__exec`), but the command grammar is the same. Tool names and schemas are not predictable, so discover and inspect before you call. + +**Grammar** — run in this order: + +```text +exec({ "command": "search " }) # find tools by name/title/description; `tools` lists them all +exec({ "command": "info " }) # REQUIRED before every call — description + input schema +exec({ "command": "schema " }) # drill into a field the schema flags with a `hint` +exec({ "command": "call " }) # run the tool +``` + +Running `info ` before `call ` is mandatory, the same way you read a file before editing it. `info` returns the full schema for simple tools; for large ones it summarizes and attaches `hint` entries pointing at fields to drill into with `schema`. Dot-notation descends objects (`query.source`), array items (`series.0.properties`), and unions. Never guess the structure of a field that carries a hint — drill first. + +Every PostHog tool goes through `exec` this way — there is no separate named tool to call directly. The inner tool names and JSON payloads below are what you pass to `call`. + +**Errors** carry a suggestion and similar tool names — read it before retrying. If a name isn't found it may have been renamed; run `search ` or `tools` again to find the current one. + +Create the parent dashboard first with `dashboard-create`, capture its returned `id`, then attach every insight to it via `dashboards: []`: + +```json +{ + "name": "Analytics basics (wizard)", + "description": "Key views for the events instrumented by the PostHog wizard.", + "tags": ["wizard"] +} +``` + +When calling `insight-create`, use these known-good query shapes — they are verified against the MCP schema, and the common variations around them are rejected: + +A trends insight with a breakdown (breakdowns go in `breakdownFilter.breakdowns`, an array — there is NO top-level `breakdown` field on `TrendsQuery`): + +```json +{ + "name": "Signups by plan (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "TrendsQuery", + "series": [{ "kind": "EventsNode", "event": "user_signed_up", "math": "total" }], + "interval": "day", + "dateRange": { "date_from": "-30d" }, + "breakdownFilter": { "breakdowns": [{ "type": "event", "property": "plan" }] }, + "trendsFilter": { "display": "ActionsBar" } + } + } +} +``` + +A conversion funnel (the window fields are camelCase and live INSIDE `funnelsFilter` — not at the top level of `FunnelsQuery`, and not snake_case): + +```json +{ + "name": "Signup funnel (wizard)", + "dashboards": [], + "query": { + "kind": "InsightVizNode", + "source": { + "kind": "FunnelsQuery", + "series": [ + { "kind": "EventsNode", "event": "page_viewed" }, + { "kind": "EventsNode", "event": "user_signed_up" } + ], + "dateRange": { "date_from": "-30d" }, + "funnelsFilter": { + "funnelVizType": "steps", + "funnelOrderType": "ordered", + "funnelWindowInterval": 14, + "funnelWindowIntervalUnit": "day" + } + } + } +} +``` + +Valid `trendsFilter.display` values are `ActionsLineGraph`, `ActionsBar`, `ActionsAreaGraph`, `ActionsPie`, `ActionsStackedBar`, `BoldNumber`, and `ActionsTable` — names like `ActionsBarChart` or `ActionsBarGraph` are rejected. If an insight call is rejected anyway, fix the payload against these examples rather than retrying variations. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Compose the setup report as markdown — do NOT write it to a file in the project. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Then publish the report to the wizard session with a single `publish_handoff` call, passing the complete report markdown as `content`. This call is how the report reaches the user — do not write it to a file instead. + +Then mirror the report into a shareable PostHog notebook so the user has an in-app copy to link and comment on. Call `notebooks-create` with a `title` (e.g. `PostHog setup (wizard) – `) and `content` set to a single markdown node wrapping the report verbatim — `{"type":"doc","content":[{"type":"ph-markdown-notebook","attrs":{"nodeId":"markdown-notebook-v2","markdown":""}}]}`. Take the `short_id` from the response, build the notebook URL as `/project//notebooks/`, and emit it on its own line so the wizard can surface it: `[NOTEBOOK_URL]` followed by that URL. + +Upon completion, update `.posthog-events.json` so it matches the events you actually implemented, then remove it with your file tools. If removal is blocked or fails in your environment, leave the file in place and move on — the wizard host cleans it up after the run. Do not retry the removal or reach for shell commands to force it. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Published setup report to the wizard session +- Created notebook: [insert PostHog notebook URL] \ No newline at end of file diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/COMMANDMENTS.md b/skills/posthog/all/skills/integration-astro-hybrid/references/COMMANDMENTS.md new file mode 100644 index 00000000..70585e85 --- /dev/null +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/COMMANDMENTS.md @@ -0,0 +1,37 @@ +# Framework rules + +Follow these when integrating PostHog into this framework. + +- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op +- Always use the is:inline directive on PostHog script tags to prevent Astro from processing them and causing TypeScript errors +- Use PUBLIC_ prefix for client-side environment variables in Astro (e.g., PUBLIC_POSTHOG_PROJECT_TOKEN) +- Create a posthog.astro component in src/components/ for reusable initialization across pages +- Import the PostHog component in a Layout and wrap all pages with that layout +- 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 +- Set tracing_headers to the backend hostname on the client (hostnames only, no port) so posthog-js sends X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID on every request, and read both server-side so its events attribute to the same person +- 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 +- Remember that source code is available in the node_modules directory +- Check package.json for type checking or build scripts to validate changes +- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it +- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). +- posthog-js is the JavaScript SDK package name +- posthog.init() MUST be called before any other PostHog methods (capture, identify, etc.) +- posthog-js is browser-only — do NOT import it in Node.js or server-side contexts (use posthog-node instead) +- Autocapture is ON by default with posthog-js (tracks clicks, form submissions, pageviews). Keep autocapture enabled unless the user explicitly asks to turn it off. +- NEVER send PII in posthog.capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in posthog.identify() person properties (email, name, role), NOT in capture() event properties +- Call posthog.identify(userId, { email, name, role }) on login AND on page refresh if the user is already logged in +- Call posthog.reset() on logout — the transition out of an identified session, never an initially anonymous page load (that discards the anonymous id and its history) — and before identify() when switching directly between accounts +- For SPAs without a framework router, capture pageviews with posthog.capture($pageview) or use the capture_pageview history_change option in init for History API routing +- When verifying with an automated browser (Playwright, Puppeteer, Selenium), posthog-js's bot filter silently drops every capture while flags and asset loads still succeed. Override navigator.webdriver, the user agent, AND navigator.userAgentData before concluding events do not send. Diagnose with ?__posthog_debug=true ("likely bot" in the console). +- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead +- 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')) +- 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 diff --git a/skills/posthog/all/skills/integration-astro-hybrid/references/EXAMPLE.md b/skills/posthog/all/skills/integration-astro-hybrid/references/EXAMPLE.md index 74cb78e6..41930922 100644 --- a/skills/posthog/all/skills/integration-astro-hybrid/references/EXAMPLE.md +++ b/skills/posthog/all/skills/integration-astro-hybrid/references/EXAMPLE.md @@ -1,7 +1,7 @@ # PostHog Astro (Hybrid) Example Project Repository: https://github.com/PostHog/context-mill -Path: basics/astro-hybrid +Path: example-apps/astro-hybrid --- @@ -24,14 +24,14 @@ This shows how to: - Opt specific pages into SSR with `export const prerender = false` - Keep most pages static for performance - Track events from API routes using `posthog-node` -- Pass session IDs from client to server for unified sessions +- Link client and server sessions automatically with the `tracing_headers` option ## Features - **Hybrid rendering**: Static pages by default, SSR when needed - **API routes**: Server-side endpoints for auth and event tracking - **Dual tracking**: Events captured on both client and server -- **Session continuity**: Session ID passed to server via headers +- **Session continuity**: Session and distinct ID forwarded automatically via `tracing_headers` - **Product analytics**: Track login and burrito consideration events - **Error tracking**: Manual error capture sent to PostHog @@ -192,6 +192,31 @@ npm run preview --- +## .astro/content-assets.mjs + +```mjs +export default new Map(); +``` + +--- + +## .astro/content-modules.mjs + +```mjs +export default new Map(); +``` + +--- + +## .astro/types.d.ts + +```ts +/// + +``` + +--- + ## .env.example ```example @@ -358,7 +383,10 @@ export default defineConfig({ !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n @@ -559,6 +587,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, @@ -618,6 +649,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, @@ -718,15 +752,13 @@ export const prerender = false; source: 'client' }); - // Also send to server-side API for server tracking + // Also send to server-side API for server tracking. The session and distinct + // ID are added automatically by the tracing_headers option in posthog.init. try { - const sessionId = window.posthog?.get_session_id?.() || null; - await fetch('/api/events/burrito', { method: 'POST', headers: { - 'Content-Type': 'application/json', - 'X-PostHog-Session-Id': sessionId || '' + 'Content-Type': 'application/json' }, body: JSON.stringify({ username: currentUser, @@ -763,7 +795,7 @@ import PostHogLayout from '../layouts/PostHogLayout.astro';