This is a bare React Native example (no Expo) demonstrating PostHog integration with product analytics, user identification, autocapture, and error tracking.
- Product analytics: Track user events and behaviors
- Autocapture: Automatic touch event and screen view tracking
- Error tracking: Capture and track errors manually
- User authentication: Demo login system with PostHog user identification
- Session persistence: AsyncStorage for maintaining user sessions across app restarts
- Native navigation: React Navigation v7 with native stack navigator
You need a Mac with the following installed:
-
Xcode (from the Mac App Store)
- Open App Store and search for "Xcode"
- Install it (~12GB download)
- After installing, open Xcode once to accept the license agreement
-
Xcode Command Line Tools
xcode-select --install
-
CocoaPods (iOS dependency manager)
brew install cocoapods
Or without Homebrew:
sudo gem install cocoapods
-
Android Studio (the Android IDE)
brew install --cask android-studio
Or download from: https://developer.android.com/studio
-
First-time Android Studio Setup
- Open Android Studio
- Complete the setup wizard (downloads Android SDK automatically)
- Go to Settings → Languages & Frameworks → Android SDK
- Ensure "Android SDK Platform 34" (or latest) is installed
-
Create an Android Emulator
- In Android Studio: Tools → Device Manager
- Click Create Device
- Select a phone (e.g., "Pixel 7")
- Download a system image (e.g., API 34)
- Finish and click the Play button to launch
-
Environment Variables (add to
~/.zshrcor~/.bashrc)# Android SDK export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/emulator export PATH=$PATH:$ANDROID_HOME/platform-tools # Java from Android Studio (required for Gradle) export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" export PATH=$JAVA_HOME/bin:$PATH
Then run
source ~/.zshrcto apply. -
Create local.properties file (if SDK location is not detected) Create
android/local.propertieswith:sdk.dir=$HOME/Library/Android/sdk -
Clear Gradle cache (required when jumping between different versions of Gradle)
rm -rf ~/.gradle/caches/modules-2/files-2.1/org.gradle.toolchains/foojay-resolver
npm installCreate a .env file:
cp .env.example .envEdit .env and add your PostHog project token:
POSTHOG_PROJECT_TOKEN=phc_your_project_token_here
POSTHOG_HOST=https://us.i.posthog.comGet your PostHog project token from your PostHog project settings.
Note: The app will still run without a PostHog project token - analytics will simply be disabled.
Install iOS dependencies (first time only):
cd ios && pod install && cd ..Run the app:
npm run iosNote: First build takes 5-10 minutes. Subsequent builds are much faster.
Make sure an Android emulator is running (from Android Studio Device Manager), then:
npm run androidNote: First build takes 3-5 minutes.
"No `Podfile' found"
- Make sure you're in the
iosdirectory:cd ios && pod install
Build fails with signing errors
- Open
ios/BurritoApp.xcworkspacein Xcode - Select the project → Signing & Capabilities
- Select your development team
Simulator not launching
- Open Xcode → Open Developer Tool → Simulator
- Or run:
open -a Simulator
"SDK location not found"
- Ensure
ANDROID_HOMEis set in your shell profile - Run
source ~/.zshrcafter adding it
"No connected devices"
- Launch an emulator from Android Studio Device Manager
- Or connect a physical device with USB debugging enabled
Gradle build fails
- Try:
cd android && ./gradlew clean && cd .. - Then:
npm run android
src/
├── config/
│ └── posthog.ts # PostHog client configuration
├── contexts/
│ └── AuthContext.tsx # Authentication context with PostHog integration
├── navigation/
│ └── RootNavigator.tsx # React Navigation stack navigator
├── screens/
│ ├── HomeScreen.tsx # Home/login screen
│ ├── BurritoScreen.tsx # Demo feature screen with event tracking
│ └── ProfileScreen.tsx # User profile with error tracking demo
├── services/
│ └── storage.ts # AsyncStorage wrapper for persistence
├── styles/
│ └── theme.ts # Shared style constants
└── types/
└── env.d.ts # Type declarations for environment variables
App.tsx # Root component with PostHogProvider
index.js # App entry point
.env # Environment variables (create from .env.example)
ios/ # Native iOS project (Xcode)
android/ # Native Android project (Android Studio)
The PostHog client is configured with V4 SDK options. If no project token is provided, analytics are disabled gracefully:
import PostHog from 'posthog-react-native'
import Config from 'react-native-config'
const projectToken = Config.POSTHOG_PROJECT_TOKEN
const isPostHogConfigured = projectToken && projectToken !== 'phc_your_project_token_here'
export const posthog = new PostHog(projectToken || 'placeholder_key', {
host: Config.POSTHOG_HOST || 'https://us.i.posthog.com',
disabled: !isPostHogConfigured, // Disable if no project token
captureAppLifecycleEvents: true,
debug: __DEV__,
flushAt: 20,
flushInterval: 10000,
preloadFeatureFlags: true,
})For React Navigation v7, PostHogProvider must be placed inside NavigationContainer, and screen tracking must be done manually:
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
import { PostHogProvider } from 'posthog-react-native'
import { posthog } from './src/config/posthog'
export default function App() {
const navigationRef = useRef<NavigationContainerRef<RootStackParamList>>(null)
const routeNameRef = useRef<string | undefined>()
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {
routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name
}}
onStateChange={() => {
// Manual screen tracking for React Navigation v7
const previousRouteName = routeNameRef.current
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name
if (previousRouteName !== currentRouteName && currentRouteName) {
posthog.screen(currentRouteName, {
previous_screen: previousRouteName,
})
}
routeNameRef.current = currentRouteName
}}
>
<PostHogProvider
client={posthog}
autocapture={{
captureScreens: false, // Disabled for React Navigation v7
captureTouches: true, // Enable touch event autocapture
propsToCapture: ['testID'],
}}
>
<AuthProvider>
<RootNavigator />
</AuthProvider>
</PostHogProvider>
</NavigationContainer>
)
}PostHog autocapture automatically tracks:
- Touch events: When users interact with the screen
- App lifecycle events: Application Installed, Updated, Opened, Became Active, Backgrounded
Use testID prop on components to help identify them in analytics:
<TouchableOpacity testID="consider-burrito-button" onPress={handlePress}>
<Text>Consider Burrito</Text>
</TouchableOpacity>Use $set and $set_once for person properties:
import { usePostHog } from 'posthog-react-native'
const posthog = usePostHog()
// On login - identify with person properties
posthog.identify(username, {
$set: {
username: username,
},
$set_once: {
first_login_date: new Date().toISOString(),
},
})
// Capture login event
posthog.capture('user_logged_in', {
username: username,
is_new_user: isNewUser,
})
// On logout - reset clears distinct ID and anonymous ID
posthog.capture('user_logged_out')
posthog.reset()Capture custom events with properties:
import { usePostHog } from 'posthog-react-native'
const posthog = usePostHog()
// We recommend using a [object] [verb] format for event names
posthog.capture('burrito_considered', {
total_considerations: user.burritoConsiderations + 1,
username: user.username,
})Capture exceptions using captureException:
import { usePostHog } from 'posthog-react-native'
const posthog = usePostHog()
try {
throw new Error('Test error for PostHog error tracking')
} catch (err) {
posthog.captureException(err)
}AsyncStorage replaces localStorage for persisting user sessions:
import AsyncStorage from '@react-native-async-storage/async-storage'
export const storage = {
getCurrentUser: async (): Promise<string | null> => {
return await AsyncStorage.getItem('currentUser')
},
setCurrentUser: async (username: string): Promise<void> => {
await AsyncStorage.setItem('currentUser', username)
},
saveUser: async (user: User): Promise<void> => {
const users = await storage.getUsers()
users[user.username] = user
await AsyncStorage.setItem('users', JSON.stringify(users))
},
}