📊 Analytics System
Index
- Analytics Options
- Expo Analytics
- Firebase Analytics
- Mixpanel
- PostHog
- Analytics Context
- Standard Events
- Best Practices
Analytics Options
| Option | Complexity | Best for | Pros | Cons |
|---|
| Firebase Analytics | Low | Production apps | Free, robust, integrated | Google ecosystem |
| Mixpanel | Medium | Product analytics | Funnels, retention | Costly at scale |
| PostHog | Medium | Open source, privacy | Self-host available | More setup |
| Amplitude | Medium | Growth teams | Very complete | Costly |
| Custom Backend | High | Full control | No dependencies | More development |
Expo Analytics
Instalación
npx expo install expo-application expo-constants
Basic Implementation
// lib/analytics.ts
import * as Application from 'expo-application';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AnalyticsEvent {
name: string;
properties?: Record<string, unknown>;
timestamp: string;
sessionId: string;
userId?: string;
}
const EVENTS_KEY = 'analytics_events';
const SESSION_KEY = 'analytics_session';
let sessionId: string | null = null;
let userId: string | null = null;
export async function initAnalytics() {
sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2)}`;
await AsyncStorage.setItem(SESSION_KEY, sessionId);
}
export function setUserId(id: string | null) {
userId = id;
}
export async function trackEvent(
name: string,
properties?: Record<string, unknown>
) {
const event: AnalyticsEvent = {
name,
properties: {
...properties,
platform: Platform.OS,
appVersion: Application.nativeApplicationVersion,
buildVersion: Application.nativeBuildVersion,
},
timestamp: new Date().toISOString(),
sessionId: sessionId || 'unknown',
userId: userId || undefined,
};
console.log('[Analytics]', event.name, event.properties);
// Store locally for batching
const stored = await AsyncStorage.getItem(EVENTS_KEY);
const events: AnalyticsEvent[] = stored ? JSON.parse(stored) : [];
events.push(event);
await AsyncStorage.setItem(EVENTS_KEY, JSON.stringify(events));
// Flush if too many events
if (events.length >= 10) {
await flushEvents();
}
}
export async function flushEvents() {
const stored = await AsyncStorage.getItem(EVENTS_KEY);
if (!stored) return;
const events: AnalyticsEvent[] = JSON.parse(stored);
if (events.length === 0) return;
try {
// Send to your analytics endpoint
// await fetch('YOUR_ANALYTICS_ENDPOINT', {
// method: 'POST',
// body: JSON.stringify({ events }),
// });
await AsyncStorage.setItem(EVENTS_KEY, '[]');
} catch (error) {
console.error('Failed to flush analytics:', error);
}
}
export async function trackScreen(screenName: string) {
await trackEvent('screen_view', { screen_name: screenName });
}
Firebase Analytics
Instalación
npx expo install @react-native-firebase/app @react-native-firebase/analytics
Configuración
// lib/firebaseAnalytics.ts
import analytics from '@react-native-firebase/analytics';
export const firebaseAnalytics = {
async logEvent(name: string, params?: Record<string, unknown>) {
await analytics().logEvent(name, params);
},
async logScreenView(screenName: string, screenClass?: string) {
await analytics().logScreenView({
screen_name: screenName,
screen_class: screenClass || screenName,
});
},
async setUserId(userId: string | null) {
await analytics().setUserId(userId);
},
async setUserProperties(properties: Record<string, string>) {
for (const [key, value] of Object.entries(properties)) {
await analytics().setUserProperty(key, value);
}
},
async logLogin(method: string) {
await analytics().logLogin({ method });
},
async logSignUp(method: string) {
await analytics().logSignUp({ method });
},
async logPurchase(params: {
currency: string;
value: number;
items?: Array<{ item_id: string; item_name: string; price: number }>;
}) {
await analytics().logPurchase(params);
},
async logAddToCart(params: {
currency: string;
value: number;
items: Array<{ item_id: string; item_name: string; price: number }>;
}) {
await analytics().logAddToCart(params);
},
async logShare(params: { content_type: string; item_id: string; method: string }) {
await analytics().logShare(params);
},
async logSearch(searchTerm: string) {
await analytics().logSearch({ search_term: searchTerm });
},
};
Mixpanel
Instalación
npx expo install mixpanel-react-native
Configuración
// lib/mixpanel.ts
import { Mixpanel } from 'mixpanel-react-native';
const MIXPANEL_TOKEN = process.env.EXPO_PUBLIC_MIXPANEL_TOKEN!;
const mixpanel = new Mixpanel(MIXPANEL_TOKEN, true);
export async function initMixpanel() {
await mixpanel.init();
}
export const mixpanelAnalytics = {
track(event: string, properties?: Record<string, unknown>) {
mixpanel.track(event, properties);
},
identify(userId: string) {
mixpanel.identify(userId);
},
setProfile(properties: Record<string, unknown>) {
mixpanel.getPeople().set(properties);
},
reset() {
mixpanel.reset();
},
timeEvent(eventName: string) {
mixpanel.timeEvent(eventName);
},
trackWithGroups(
event: string,
properties: Record<string, unknown>,
groups: Record<string, string>
) {
mixpanel.trackWithGroups(event, properties, groups);
},
setGroup(groupKey: string, groupId: string) {
mixpanel.setGroup(groupKey, groupId);
},
registerSuperProperties(properties: Record<string, unknown>) {
mixpanel.registerSuperProperties(properties);
},
};
PostHog
Instalación
npx expo install posthog-react-native
Configuración
// lib/posthog.ts
import PostHog from 'posthog-react-native';
const POSTHOG_API_KEY = process.env.EXPO_PUBLIC_POSTHOG_API_KEY!;
const POSTHOG_HOST = process.env.EXPO_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com';
let posthog: PostHog | null = null;
export async function initPostHog() {
posthog = await PostHog.initAsync(POSTHOG_API_KEY, {
host: POSTHOG_HOST,
});
}
export const posthogAnalytics = {
capture(event: string, properties?: Record<string, unknown>) {
posthog?.capture(event, properties);
},
screen(screenName: string, properties?: Record<string, unknown>) {
posthog?.screen(screenName, properties);
},
identify(userId: string, properties?: Record<string, unknown>) {
posthog?.identify(userId, properties);
},
alias(alias: string) {
posthog?.alias(alias);
},
reset() {
posthog?.reset();
},
isFeatureEnabled(key: string): boolean {
return posthog?.isFeatureEnabled(key) ?? false;
},
getFeatureFlag(key: string): string | boolean | undefined {
return posthog?.getFeatureFlag(key);
},
reloadFeatureFlags() {
posthog?.reloadFeatureFlagsAsync();
},
flush() {
posthog?.flush();
},
};
Analytics Context
// contexts/AnalyticsContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useEffect, useCallback, useRef } from 'react';
import { AppState, AppStateStatus, Platform } from 'react-native';
import * as Application from 'expo-application';
import AsyncStorage from '@react-native-async-storage/async-storage';
type AnalyticsProvider = 'firebase' | 'mixpanel' | 'posthog' | 'custom';
interface AnalyticsEvent {
name: string;
properties?: Record<string, unknown>;
timestamp: string;
}
const EVENTS_KEY = 'analytics_queue';
const USER_ID_KEY = 'analytics_user_id';
export const [AnalyticsProvider, useAnalytics] = createContextHook(() => {
const sessionStart = useRef(Date.now());
const userId = useRef<string | null>(null);
const eventsQueue = useRef<AnalyticsEvent[]>([]);
useEffect(() => {
AsyncStorage.getItem(USER_ID_KEY).then((id) => {
userId.current = id;
});
const subscription = AppState.addEventListener('change', handleAppStateChange);
track('session_start', {
platform: Platform.OS,
app_version: Application.nativeApplicationVersion,
});
return () => {
subscription.remove();
flush();
};
}, []);
const handleAppStateChange = (state: AppStateStatus) => {
if (state === 'background') {
const duration = Math.round((Date.now() - sessionStart.current) / 1000);
track('session_end', { duration_seconds: duration });
flush();
} else if (state === 'active') {
sessionStart.current = Date.now();
track('session_start');
}
};
const track = useCallback((name: string, properties?: Record<string, unknown>) => {
const event: AnalyticsEvent = {
name,
properties: {
...properties,
user_id: userId.current,
platform: Platform.OS,
timestamp: new Date().toISOString(),
},
timestamp: new Date().toISOString(),
};
console.log('[Analytics]', name, properties);
eventsQueue.current.push(event);
if (eventsQueue.current.length >= 10) {
flush();
}
}, []);
const trackScreen = useCallback((screenName: string, properties?: Record<string, unknown>) => {
track('screen_view', { screen_name: screenName, ...properties });
}, [track]);
const identify = useCallback(async (id: string, traits?: Record<string, unknown>) => {
userId.current = id;
await AsyncStorage.setItem(USER_ID_KEY, id);
track('identify', { user_id: id, ...traits });
}, [track]);
const reset = useCallback(async () => {
userId.current = null;
await AsyncStorage.removeItem(USER_ID_KEY);
track('logout');
}, [track]);
const flush = useCallback(async () => {
if (eventsQueue.current.length === 0) return;
const events = [...eventsQueue.current];
eventsQueue.current = [];
try {
// Send to your analytics backend
// await fetch('YOUR_ANALYTICS_ENDPOINT', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ events }),
// });
// Or use a third-party provider
// events.forEach(e => firebase.analytics().logEvent(e.name, e.properties));
console.log('[Analytics] Flushed', events.length, 'events');
} catch (error) {
console.error('[Analytics] Flush failed:', error);
eventsQueue.current = [...events, ...eventsQueue.current];
}
}, []);
const trackPurchase = useCallback((params: {
productId: string;
price: number;
currency: string;
source?: string;
}) => {
track('purchase', {
product_id: params.productId,
price: params.price,
currency: params.currency,
source: params.source,
});
}, [track]);
const trackError = useCallback((error: Error, context?: Record<string, unknown>) => {
track('error', {
error_message: error.message,
error_name: error.name,
error_stack: error.stack?.slice(0, 500),
...context,
});
}, [track]);
return {
track,
trackScreen,
trackPurchase,
trackError,
identify,
reset,
flush,
};
});
Standard Events
User Events
// Authentication
track('sign_up', { method: 'email' | 'google' | 'apple' });
track('login', { method: 'email' | 'google' | 'apple' });
track('logout');
track('password_reset_requested');
// Profile
track('profile_updated', { fields: ['name', 'avatar'] });
track('settings_changed', { setting: 'notifications', value: true });
Navigation Events
trackScreen('Home');
trackScreen('ProductDetail', { product_id: '123' });
track('tab_changed', { from: 'home', to: 'explore' });
track('modal_opened', { modal: 'filters' });
Engagement Events
track('item_viewed', { item_id: '123', category: 'tech' });
track('item_favorited', { item_id: '123' });
track('item_shared', { item_id: '123', method: 'copy_link' });
track('search', { query: 'react native', results_count: 15 });
track('filter_applied', { filters: { category: 'tech', price: 'free' } });
Monetization Events
track('paywall_viewed', { source: 'feature_gate' });
track('subscription_started', { plan: 'premium_monthly', price: 9.99 });
track('subscription_cancelled', { plan: 'premium_monthly', reason: 'too_expensive' });
track('purchase_completed', { product_id: 'credits_100', price: 4.99 });
Error Events
trackError(new Error('API call failed'), { endpoint: '/api/items' });
track('error_displayed', { error_type: 'network', screen: 'Home' });
Best Practices
1. Naming Conventions
// ✅ Good - snake_case, descriptive
track('button_clicked', { button_name: 'submit' });
track('form_submitted', { form_name: 'contact' });
// ❌ Bad - inconsistent, vague
track('Click', { btn: 'sub' });
track('FormSubmit');
2. Consistent Properties
// Define standard properties
interface StandardProperties {
screen_name?: string;
user_id?: string;
session_id?: string;
timestamp?: string;
}
// Always include relevant context
track('item_purchased', {
item_id: '123',
item_name: 'Premium Plan',
price: 9.99,
currency: 'USD',
screen_name: 'Paywall', // ← Contexto
});
3. Avoid Over-Tracking
// ❌ Don't track every keystroke
onChangeText={(text) => track('text_input', { text })}
// ✅ Track meaningful actions
onSubmit={() => track('search_submitted', { query })}
4. Privacy First
// Don't include PII directly
// ❌ Bad
track('user_profile', { email: 'user@email.com', phone: '+1234567890' });
// ✅ Good - use hashed or anonymized IDs
track('user_profile', { user_id: 'usr_abc123' });
5. Hook for Automatic Screen Tracking
// hooks/useScreenTracking.ts
import { useEffect } from 'react';
import { usePathname } from 'expo-router';
import { useAnalytics } from '@/contexts/AnalyticsContext';
export function useScreenTracking() {
const pathname = usePathname();
const { trackScreen } = useAnalytics();
useEffect(() => {
const screenName = pathname
.replace(/\//g, '_')
.replace(/^\(tabs\)_?/, '')
.replace(/^_/, '') || 'Home';
trackScreen(screenName);
}, [pathname, trackScreen]);
}
Implementation Checklist