Setting Up PostHog Analytics in Mobile Apps
A practical guide to PostHog analytics in React Native, covering event taxonomy, custom properties, dashboards, and privacy-friendly tracking.
## Why PostHog for Mobile
Most analytics tools are built for web. PostHog started there too, but it's become one of the best options for mobile apps. Here's why:
- **Self-hostable or cloud**: Choose your data residency - **Event-based**: Track anything, not just page views - **Feature flags**: Roll out features to a percentage of users - **Session replay**: Watch how users navigate your app (web only for now, mobile coming) - **Generous free tier**: 1 million events per month
We track analytics in every app we build. PostHog is our default choice. Here's exactly how to set it up in React Native.
## Installation
```bash npx expo install posthog-react-native ```
## Initialization (The Right Way)
The most common mistake with PostHog in React Native is initializing with an empty API key. If the `EXPO_PUBLIC_POSTHOG_API_KEY` environment variable is not set (common in development), creating a PostHog instance with an empty string will crash your app.
```typescript // lib/posthog.ts import PostHog from 'posthog-react-native';
const apiKey = process.env.EXPO_PUBLIC_POSTHOG_API_KEY; const host = process.env.EXPO_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com';
// Only create the client if we have a valid API key export const posthog = apiKey ? new PostHog(apiKey, { host }) : null; ```
Guard every analytics call:
```typescript export function capture(event: string, properties?: Record<string, unknown>) { if (!posthog) return; posthog.capture(event, properties); }
export function identify(userId: string, properties?: Record<string, unknown>) { if (!posthog) return; posthog.identify(userId, properties); }
export function reset() { if (!posthog) return; posthog.reset(); } ```
This pattern ensures your app works in any environment, with or without analytics configured.
## Designing Your Event Taxonomy
Before you start tracking, plan your events. A messy event taxonomy is worse than no analytics at all. You'll spend more time cleaning data than learning from it.
### The Event Naming Convention
Use `object_action` format with snake_case:
- `app_opened` - `sign_up_completed` - `thread_viewed` - `thread_bookmarked` - `paywall_shown` - `purchase_completed` - `settings_theme_changed`
Not:
- `AppOpened` (inconsistent with PostHog conventions) - `click_button_3` (meaningless without context) - `user did a thing` (not queryable)
### Essential Events for Any Mobile App
Every app should track these at minimum:
```typescript // App lifecycle capture('app_opened'); capture('app_backgrounded');
// Authentication capture('sign_up_started', { method: 'email' }); capture('sign_up_completed', { method: 'email' }); capture('sign_in_completed', { method: 'google' }); capture('sign_out');
// Core feature usage capture('feature_used', { feature: 'thread_analysis', screen: 'dashboard' });
// Monetization capture('paywall_shown', { trigger: 'feature_gate', feature: 'ai_analysis' }); capture('purchase_started', { product: 'premium_monthly' }); capture('purchase_completed', { product: 'premium_monthly', price: 4.99 });
// Errors capture('error_occurred', { type: 'api_error', endpoint: '/threads', status: 500 }); ```
### Custom Properties
Attach context to every event. Properties answer "what happened" and "where":
```typescript capture('thread_viewed', { thread_id: thread.id, subreddit: thread.subreddit, score: thread.score, source: 'trending_feed', // How did they get here? }); ```
Good properties: - Identify the object (ID, name, category) - Describe the context (screen, source, trigger) - Include relevant metrics (score, count, duration)
Avoid: - Personal identifiable information (email, name) unless you've got consent - High-cardinality free text (full URLs, user input) - Redundant data (don't include user ID in every event; PostHog handles this)
## User Identification
Link anonymous events to authenticated users:
```typescript // After sign-in or sign-up identify(user.id, { email: user.email, plan: 'free', // or 'premium' signed_up_at: user.created_at, platform: Platform.OS, }); ```
Call `identify` once after authentication. PostHog will associate all previous anonymous events with this user (if you're using the same device).
When the user signs out:
```typescript reset(); // Clears the identified user, creates a new anonymous ID ```
## Building a useAnalytics Hook
Wrap PostHog in a custom hook that makes tracking easy across your app:
```typescript // hooks/useAnalytics.ts import { useCallback } from 'react'; import { capture, identify, reset } from '../lib/posthog';
export function useAnalytics() { const trackEvent = useCallback((event: string, properties?: Record<string, unknown>) => { capture(event, { ...properties, timestamp: new Date().toISOString(), }); }, []);
const trackScreen = useCallback((screenName: string) => { capture('screen_viewed', { screen: screenName }); }, []);
const identifyUser = useCallback((userId: string, traits?: Record<string, unknown>) => { identify(userId, traits); }, []);
const resetUser = useCallback(() => { reset(); }, []);
return { trackEvent, trackScreen, identifyUser, resetUser }; } ```
## Setting Up Dashboards
In PostHog, create a dashboard for your app with these panels:
### Daily Active Users - Event: `app_opened` - Aggregation: Unique users per day - This is your north star metric
### Signup Funnel 1. `sign_up_started` 2. `sign_up_completed` 3. `onboarding_completed` 4. `first_feature_used`
Seeing where users drop off in this funnel tells you exactly where to focus improvement.
### Feature Adoption - Event: `feature_used` - Breakdown by: `feature` property - Shows which features get used and which are ignored
### Revenue Funnel 1. `paywall_shown` 2. `purchase_started` 3. `purchase_completed`
The conversion rate from paywall shown to purchase completed tells you if your pricing and positioning work.
### Error Tracking - Event: `error_occurred` - Breakdown by: `type` and `endpoint` - Alert if the count exceeds a threshold
## Feature Flags
PostHog feature flags let you roll out features gradually:
```typescript import { useFeatureFlag } from 'posthog-react-native';
function MyComponent() { const showNewDashboard = useFeatureFlag('new-dashboard-v2');
if (showNewDashboard) { return <NewDashboard />; } return <OldDashboard />; } ```
Use feature flags for: - Gradual rollouts (10% of users, then 50%, then 100%) - A/B testing different UI variants - Kill switches for broken features - Beta access for specific user segments
## Privacy Considerations
### GDPR Compliance
Don't track until the user consents. PostHog's `optOut` flag makes this straightforward:
```typescript // Before consent posthog?.optOut();
// After user grants consent posthog?.optIn(); ```
### Data Minimization
Only track what you need. Don't capture personal information in event properties unless it's necessary for analysis. PostHog lets you configure which properties to redact on the server side.
### Retention Policies
Set data retention in PostHog's project settings. You probably don't need event data from 3 years ago. 12 months is a reasonable default.
## Common Mistakes
1. **Tracking too many events.** Start with 10-15 core events. Add more as questions arise. You can always add events later; removing noise from existing data is harder.
2. **Not tracking enough context.** An event without properties is nearly useless. "button_clicked" tells you nothing. "purchase_started with product=premium_yearly, trigger=upgrade_banner, screen=settings" tells you everything.
3. **Forgetting to test in production.** Verify events are flowing in the PostHog dashboard after your production deploy. Development and production often have different API keys.
4. **Ignoring the data.** The best analytics setup is worthless if nobody looks at the dashboards. Set a weekly reminder to review your core metrics.
For more on building production apps with proper analytics, check out our [tech stack](/tech/posthog) and the [complete build pipeline](/features/building).