Chapter 163 · Omnibus Instrument Product Analytics
Subchapter 163.21
references/EXAMPLE-react-native.mdMarkdown46 KBView on GitHub
Repository: https://github.com/PostHog/context-mill Path: basics/react-native
This is a bare React Native (opens in a new tab) example (no Expo) demonstrating PostHog integration with product analytics, user identification, autocapture, and error tracking.
You need a Mac with the following installed:
Xcode (from the Mac App Store)
Xcode Command Line Tools
xcode-select --installCocoaPods (iOS dependency manager)
brew install cocoapodsOr without Homebrew:
sudo gem install cocoapodsAndroid Studio (the Android IDE)
brew install --cask android-studioOr download from: https://developer.android.com/studio (opens in a new tab)
First-time Android Studio Setup
Create an Android Emulator
Environment Variables (add to ~/.zshrc or ~/.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:$PATHThen run source ~/.zshrc to apply.
Create local.properties file (if SDK location is not detected)
Create android/local.properties with:
sdk.dir=$HOME/Library/Android/sdkClear Gradle cache (required when jumping between different versions of Gradle)
rm -rf ~/.gradle/caches/modules-2/files-2.1/org.gradle.toolchains/foojay-resolvernpm 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 (opens in a new tab).
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”
ios directory: cd ios && pod installBuild fails with signing errors
ios/BurritoApp.xcworkspace in XcodeSimulator not launching
open -a Simulator“SDK location not found”
ANDROID_HOME is set in your shell profilesource ~/.zshrc after adding it“No connected devices”
Gradle build fails
cd android && ./gradlew clean && cd ..npm run androidsrc/
├── 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 apiKey = Config.POSTHOG_PROJECT_TOKEN
const isPostHogConfigured = apiKey && apiKey !== 'phc_your_project_token_here'
export const posthog = new PostHog(apiKey || '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:
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 the $exception event:
import { usePostHog } from 'posthog-react-native'
const posthog = usePostHog()
try {
throw new Error('Test error for PostHog error tracking')
} catch (err) {
posthog.capture('$exception', {
$exception_type: err.name,
$exception_message: err.message,
$exception_source: 'ProfileScreen',
$exception_stack_trace_raw: err.stack,
})
}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))
},
}/**
* @format
*/
import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import App from '../App';
test('renders correctly', async () => {
await ReactTestRenderer.act(() => {
ReactTestRenderer.create(<App />);
});
});
POSTHOG_PROJECT_TOKEN=phc_your_project_token_here
POSTHOG_HOST=https://us.i.posthog.com
module.exports = {
arrowParens: 'avoid',
singleQuote: true,
trailingComma: 'all',
};
import React, { useRef } from 'react'
import { StatusBar } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import {
NavigationContainer,
NavigationContainerRef,
} from '@react-navigation/native'
import { PostHogProvider } from 'posthog-react-native'
import { AuthProvider } from './src/contexts/AuthContext'
import { RootNavigator, RootStackParamList } from './src/navigation/RootNavigator'
import { posthog } from
module.exports = {
presets: ['module:@react-native/babel-preset'],
};
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
/**
* @format
*/
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
module.exports = {
preset: 'react-native',
};
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
/**
* Metro configuration
* https://reactnative.dev/docs/metro
*
* @type {import('@react-native/metro-config').MetroConfig}
*/
const config = {};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
import PostHog from 'posthog-react-native'
import Config from 'react-native-config'
// Environment variables are embedded at build time via react-native-config
// Ensure .env file exists with POSTHOG_PROJECT_TOKEN and POSTHOG_HOST
const apiKey = Config.POSTHOG_PROJECT_TOKEN
const host = Config.POSTHOG_HOST || 'https://us.i.posthog.com'
const isPostHogConfigured = apiKey && apiKey !== 'phc_your_project_token_here'
if (!isPostHogConfigured) {
console.warn(
'PostHog project token not configured. Analytics will be disabled. ' +
'Set POSTHOG_PROJECT_TOKEN in your .env file to enable analytics.'
)
}
/**
* PostHog client instance for bare React Native
*
* Configuration loaded from .env via react-native-config (embedded at build time).
* Required peer dependencies: @react-native-async-storage/async-storage,
* react-native-device-info, react-native-localize
*
* @see https://posthog.com/docs/libraries/react-native
*/
export const posthog = new PostHog(apiKey || 'placeholder_key', {
// PostHog API host (usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com')
host,
// Disable PostHog if project token is not configured
disabled: !isPostHogConfigured,
// Capture app lifecycle events:
// - Application Installed, Application Updated
// - Application Opened, Application Became Active, Application Backgrounded
captureAppLifecycleEvents: true,
// Enable debug mode in development for verbose logging
debug: __DEV__,
// Batching: queue events and flush periodically to optimize battery usage
flushAt: 20, // Number of events to queue before sending
flushInterval: 10000, // Interval in ms between periodic flushes
maxBatchSize: 100, // Maximum events per batch
maxQueueSize: 1000, // Maximum queued events (oldest dropped when full)
// Feature flags
preloadFeatureFlags: true, // Load flags on initialization
sendFeatureFlagEvent: true, // Track getFeatureFlag calls for experiments
featureFlagsRequestTimeoutMs: 10000, // Timeout for flag requests (prevents blocking)
// Network settings
requestTimeout: 10000, // General request timeout in ms
fetchRetryCount: 3, // Number of retry attempts for failed requests
fetchRetryDelay: 3000, // Delay between retries in ms
})
// Export helper to check if PostHog is enabled
export const isPostHogEnabled = isPostHogConfigured
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
useCallback,
} from 'react'
import { usePostHog } from 'posthog-react-native'
import { storage, User } from '../services/storage'
interface AuthContextType {
user: User | null
isLoading: boolean
login: (username:
import React from 'react'
import { ActivityIndicator, View, StyleSheet } from 'react-native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { useAuth } from '../contexts/AuthContext'
import { colors } from '../styles/theme'
import HomeScreen from '../screens/HomeScreen'
import BurritoScreen from '../screens/BurritoScreen'
import ProfileScreen from '../screens/ProfileScreen'
// Type definitions for navigation
export type RootStackParamList = {
Home: undefined
Burrito: undefined
Profile: undefined
}
const Stack = createNativeStackNavigator<RootStackParamList>()
export function RootNavigator() {
const { isLoading } = useAuth()
// Show loading indicator while restoring session
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
)
}
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.headerBackground,
},
headerTintColor: colors.headerText,
headerTitleStyle: {
fontWeight: 'bold',
},
headerBackTitleVisible: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: 'Burrito App',
}}
/>
<Stack.Screen
name="Burrito"
component={BurritoScreen}
options={{
title: 'Burrito Consideration',
}}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
title: 'Profile',
}}
/>
</Stack.Navigator>
)
}
const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background,
},
})
import React, { useState, useEffect } from 'react'
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { usePostHog } from 'posthog-react-native'
import { useAuth } from '../contexts/AuthContext'
import { RootStackParamList } from '../navigation/RootNavigator'
import {
colors,
spacing,
typography,
borderRadius,
import React, { useState } from 'react'
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
KeyboardAvoidingView,
Platform,
} from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useAuth } from '../contexts/AuthContext'
import { RootStackParamList } from '../navigation/RootNavigator'
import React, { useEffect } from 'react'
import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { usePostHog } from 'posthog-react-native'
import { useAuth } from '../contexts/AuthContext'
import { RootStackParamList } from '../navigation/RootNavigator'
import {
colors,
spacing,
typography,
borderRadius,
import AsyncStorage from '@react-native-async-storage/async-storage'
const CURRENT_USER_KEY = 'currentUser'
const USERS_KEY = 'users'
export interface User {
username: string
burritoConsiderations: number
}
/**
* Storage service for persisting user data
* Uses AsyncStorage (React Native's async key-value storage)
*/
export const storage = {
/**
* Get the currently logged in user's username
*/
getCurrentUser: async (): Promise<string | null> => {
try {
return await AsyncStorage.getItem(CURRENT_USER_KEY)
} catch (error) {
console.error('Error getting current user:', error)
return null
}
},
/**
* Set the currently logged in user's username
*/
setCurrentUser: async (username: string): Promise<void> => {
try {
await AsyncStorage.setItem(CURRENT_USER_KEY, username)
} catch (error) {
console.error('Error setting current user:', error)
}
},
/**
* Remove the current user (logout)
*/
removeCurrentUser: async (): Promise<void> => {
try {
await AsyncStorage.removeItem(CURRENT_USER_KEY)
} catch (error) {
console.error('Error removing current user:', error)
}
},
/**
* Get all stored users
*/
getUsers: async (): Promise<Record<string, User>> => {
try {
const data = await AsyncStorage.getItem(USERS_KEY)
return data ? JSON.parse(data) : {}
} catch (error) {
console.error('Error getting users:', error)
return {}
}
},
/**
* Get a specific user by username
*/
getUser: async (username: string): Promise<User | null> => {
try {
const users = await storage.getUsers()
return users[username] || null
} catch (error) {
console.error('Error getting user:', error)
return null
}
},
/**
* Save a user to storage
*/
saveUser: async (user: User): Promise<void> => {
try {
const users = await storage.getUsers()
users[user.username] = user
await AsyncStorage.setItem(USERS_KEY, JSON.stringify(users))
} catch (error) {
console.error('Error saving user:', error)
}
},
/**
* Clear all stored data (for testing/debugging)
*/
clearAll: async (): Promise<void> => {
try {
await AsyncStorage.multiRemove([CURRENT_USER_KEY, USERS_KEY])
} catch (error) {
console.error('Error clearing storage:', error)
}
},
}
/**
* Theme constants for consistent styling across the app
* Matches the color scheme from the TanStack Start web version
*/
export const colors = {
// Primary colors
primary: '#0070f3',
primaryDark: '#0051cc',
// Status colors
success: '#28a745',
successDark: '#218838',
danger: '#dc3545',
dangerDark: '#c82333',
// Feature colors
burrito: '#e07c24',
burritoDark: '#c96a1a',
// Neutral colors
background: '#f5f5f5',
white: '#ffffff',
text: '#333333',
textSecondary: '#666666',
textLight: '#999999',
border: '#dddddd',
borderLight: '#eeeeee',
// Component-specific
statsBackground: '#f8f9fa',
headerBackground: '#333333',
headerText: '#ffffff',
inputBackground: '#ffffff',
cardBackground: '#ffffff',
}
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
}
export const typography = {
sizes: {
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 24,
xxl: 32,
},
weights: {
normal: '400' as const,
medium: '500' as const,
semibold: '600' as const,
bold: '700' as const,
},
}
export const borderRadius = {
sm: 4,
md: 8,
lg: 12,
full: 9999,
}
export const shadows = {
sm: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
md: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
lg: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 5,
},
}
declare module 'react-native-config' {
export interface NativeConfig {
POSTHOG_PROJECT_TOKEN?: string
POSTHOG_HOST?: string
}
export const Config: NativeConfig
export default Config
}