Analytics

We don't yet have a go-to Analytics tool for mobile tracking, but we do have an approach.

I've helped implement many analytics tools over the years - some of my learnings:

  • The tool can change quite often over time, but the underlying idea (identifying users and capturing events) will remain the same - so a simple abstraction that can capture an event but can swap out providers is a good idea. Using something like Segment for this is way more trouble than it's worth - the SDK is not flexible enough to do the things you often want to do, and the cost often ends up being exorbitant. Given this is one, maybe 2 functions, we're better to set this up ourselves.

  • "Auto Capture" solutions don't work nearly as well as they claim to. For example:

    • Our apps use a combination of React Native and Webviews.

    • Some of those webviews load new pages in async, which can break/duplicate auto-firing events.

    • Several of our screens are also handled by external providers (E.g. Superwall), and we can't easily capture what happens there.

    • Autocapture doesn't have a good solution for identifying specific buttons, so you end with random strings and ids in your analytics that are undecipherable

Our Approach

Implement a service that can id a user and send an event on their behalf

// analytics.js

import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import * as Application from 'expo-application';
import { Platform } from 'react-native';
import { Mixpanel } from 'mixpanel-react-native';

const mixpanel = new Mixpanel(process.env.EXPO_PUBLIC_MIXPANEL_TOKEN,false);

class Analytics {
  constructor() {
    this.masterId = null;
    this.initialize();
  }

  async initialize() {
    this.masterId = await this.getOrCreateMasterId();
    await mixpanel.init();
  }

  async getOrCreateMasterId() {
    try {
      let masterId = await AsyncStorage.getItem('analytics_master_id');
      if (!masterId) {
        // Use platform-specific device ID methods
        if (Platform.OS === 'ios') {
          masterId = await Application.getIosIdForVendorAsync();
        } else if (Platform.OS === 'android') {
          masterId = await Application.getAndroidIdAsync();
        } else {
          // Fallback for web or other platforms
          masterId = Application.androidId || Application.applicationId || 'unknown';
        }
        await AsyncStorage.setItem('analytics_master_id', masterId);
      }
      return masterId;
    } catch (error) {
      console.error('Analytics initialization error:', error);
      return 'unknown';
    }
  }

  async getAuthData() {
    try {
      const userData = await SecureStore.getItem('userData');
      
      if (userData) {
        const user = JSON.parse(userData);
        return {
          email: user.email,
          first_name: user.first_name,
          last_name: user.last_name
        };
      }
      return { email: null, first_name: null, last_name: null };
    } catch (error) {
      return { email: null, first_name: null, last_name: null };
    }
  }

  async track(eventName, properties = {}) {
    if (!this.masterId) {
      await this.initialize();
    }

    // Get current user data
    const { email, first_name, last_name } = await this.getAuthData();
  
    this.sendMixpanelEvent(eventName, properties, { email, first_name, last_name });
  }

  async sendMixpanelEvent(eventName, properties, userData) {
    const eventProperties = {
      ...properties,
      environment: process.env.EXPO_PUBLIC_ENVIRONMENT,
      user_id: this.masterId
    };

    // Identify user in Mixpanel
    if (userData.email) {
      mixpanel.identify(this.masterId);
      mixpanel.getPeople().set({
        $email: userData.email,
        $first_name: userData.first_name,
        $last_name: userData.last_name
      });
    }

    // Track the event
    mixpanel.track(eventName, eventProperties);
    console.log('Mixpanel event sent:', eventName, eventProperties);
  }

  // Method to get masterId for use in auth flows
  async getMasterId() {
    if (!this.masterId) {
      await this.initialize();
    }
    return this.masterId;
  }
}

export default new Analytics();

Getting a user's masterID

import analytics from '@/lib/analytics';
const masterId = await analytics.getMasterId();

Tracking Events

analytics.track(`Screen Loaded`, {
    grouping: 'onboarding',
    section: currentScreen
})

Last updated