fix: switch to PostHogProvider per official React integration guide (#112)

- Install @posthog/react and wrap app with PostHogProvider
- Use VITE_PUBLIC_POSTHOG_KEY and VITE_PUBLIC_POSTHOG_HOST env vars
- Use defaults: '2026-01-30' for recommended settings
- Remove manual initAnalytics() call — Provider handles initialization
- Analytics module now checks posthog.__loaded for readiness

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #112.
This commit is contained in:
chihlasm
2026-03-16 19:18:31 -04:00
committed by GitHub
parent c44edc5088
commit 8178657632
4 changed files with 60 additions and 39 deletions

View File

@@ -13,6 +13,7 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@posthog/react": "^1.8.2",
"@sentry/react": "^10.42.0", "@sentry/react": "^10.42.0",
"@sentry/vite-plugin": "^5.1.1", "@sentry/vite-plugin": "^5.1.1",
"@stripe/stripe-js": "^8.7.0", "@stripe/stripe-js": "^8.7.0",
@@ -1597,6 +1598,22 @@
"cross-spawn": "^7.0.6" "cross-spawn": "^7.0.6"
} }
}, },
"node_modules/@posthog/react": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@posthog/react/-/react-1.8.2.tgz",
"integrity": "sha512-KzUuXIcAR8fAjU7IeDq+XfEcUTNvzgEGB381WRrFUUsu7jFTcKZZ6crx/ukHRCzTnoEuy5EJDkL7b7sJecPlCg==",
"license": "MIT",
"peerDependencies": {
"@types/react": ">=16.8.0",
"posthog-js": ">=1.257.2",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@posthog/types": { "node_modules/@posthog/types": {
"version": "1.360.2", "version": "1.360.2",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.360.2.tgz", "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.360.2.tgz",

View File

@@ -25,6 +25,7 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@posthog/react": "^1.8.2",
"@sentry/react": "^10.42.0", "@sentry/react": "^10.42.0",
"@sentry/vite-plugin": "^5.1.1", "@sentry/vite-plugin": "^5.1.1",
"@stripe/stripe-js": "^8.7.0", "@stripe/stripe-js": "^8.7.0",

View File

@@ -2,29 +2,24 @@
* PostHog product analytics wrapper. * PostHog product analytics wrapper.
* *
* Tracks key user actions to understand product usage, activation, * Tracks key user actions to understand product usage, activation,
* and engagement. All events are lightweight discrete actions — no * and engagement. All events are lightweight discrete actions.
* pageviews or mouse tracking.
* *
* Free tier: 1M events/month (more than enough for current scale). * Free tier: 1M events/month (more than enough for current scale).
*
* PostHog is initialized via PostHogProvider in main.tsx.
* This module provides typed event helpers that import posthog-js directly
* (valid per PostHog docs for non-component code).
*/ */
import posthog from 'posthog-js' import posthog from 'posthog-js'
const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY as string | undefined /** Check if PostHog has been initialized (by the Provider). */
const POSTHOG_HOST = (import.meta.env.VITE_POSTHOG_HOST as string) || 'https://us.i.posthog.com' function isReady(): boolean {
try {
let initialized = false // posthog-js sets __loaded when init completes
return !!(posthog as unknown as { __loaded?: boolean }).__loaded
/** Initialize PostHog. Call once on app startup. */ } catch {
export function initAnalytics() { return false
if (initialized || !POSTHOG_KEY) return }
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
autocapture: false, // We track events explicitly — no auto-capture
capture_pageview: false, // SPA — we'll track meaningful navigations, not every route
capture_pageleave: false,
persistence: 'localStorage',
})
initialized = true
} }
/** Identify a logged-in user. Call after login/fetchUser. */ /** Identify a logged-in user. Call after login/fetchUser. */
@@ -35,7 +30,7 @@ export function identifyUser(user: {
is_super_admin?: boolean is_super_admin?: boolean
account_id?: string account_id?: string
}) { }) {
if (!initialized) return if (!isReady()) return
posthog.identify(user.id, { posthog.identify(user.id, {
email: user.email, email: user.email,
role: user.role, role: user.role,
@@ -48,7 +43,7 @@ export function identifyUser(user: {
/** Reset identity on logout. */ /** Reset identity on logout. */
export function resetAnalytics() { export function resetAnalytics() {
if (!initialized) return if (!isReady()) return
posthog.reset() posthog.reset()
} }
@@ -56,7 +51,7 @@ export function resetAnalytics() {
/** Track a named event with optional properties. */ /** Track a named event with optional properties. */
function track(event: string, properties?: Record<string, unknown>) { function track(event: string, properties?: Record<string, unknown>) {
if (!initialized) return if (!isReady()) return
posthog.capture(event, properties) posthog.capture(event, properties)
} }

View File

@@ -1,36 +1,44 @@
import "./instrument"; // Sentry must init before any other imports import "./instrument"; // Sentry must init before any other imports
import { initAnalytics } from './lib/analytics'
initAnalytics() // PostHog product analytics
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { reactErrorHandler } from '@sentry/react' import { reactErrorHandler } from '@sentry/react'
import { HelmetProvider } from 'react-helmet-async' import { HelmetProvider } from 'react-helmet-async'
import { PostHogProvider } from '@posthog/react'
import { Toaster } from 'sonner' import { Toaster } from 'sonner'
import './index.css' import './index.css'
import App from './App' import App from './App'
const posthogOptions = {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
defaults: '2026-01-30',
} as const
createRoot(document.getElementById('root')!, { createRoot(document.getElementById('root')!, {
onUncaughtError: reactErrorHandler(), onUncaughtError: reactErrorHandler(),
onCaughtError: reactErrorHandler(), onCaughtError: reactErrorHandler(),
onRecoverableError: reactErrorHandler(), onRecoverableError: reactErrorHandler(),
}).render( }).render(
<StrictMode> <StrictMode>
<HelmetProvider> <PostHogProvider
{/* Toast notification system - theme syncs automatically via CSS custom properties */} apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY || ''}
<Toaster options={posthogOptions}
position="top-right" >
expand={false} <HelmetProvider>
closeButton {/* Toast notification system - theme syncs automatically via CSS custom properties */}
visibleToasts={3} <Toaster
gap={8} position="top-right"
theme="dark" expand={false}
toastOptions={{ closeButton
className: 'sonner-toast-custom', visibleToasts={3}
}} gap={8}
/> theme="dark"
<App /> toastOptions={{
</HelmetProvider> className: 'sonner-toast-custom',
}}
/>
<App />
</HelmetProvider>
</PostHogProvider>
</StrictMode>, </StrictMode>,
) )