📊 Analytics System

Index

  1. Analytics Options
  2. Expo Analytics
  3. Firebase Analytics
  4. Mixpanel
  5. PostHog
  6. Analytics Context
  7. Standard Events
  8. Best Practices

Analytics Options

OptionComplexityBest forProsCons
Firebase AnalyticsLowProduction appsFree, robust, integratedGoogle ecosystem
MixpanelMediumProduct analyticsFunnels, retentionCostly at scale
PostHogMediumOpen source, privacySelf-host availableMore setup
AmplitudeMediumGrowth teamsVery completeCostly
Custom BackendHighFull controlNo dependenciesMore 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 });
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

  • Choose analytics provider
  • Configure SDK and API keys
  • Create analytics context
  • Implement automatic screen tracking
  • Define standard events for your app
  • Implement error tracking
  • Configure user identification
  • Implement batching and flush
  • Verify data in dashboard
  • Document events for the team