UNPKG

mixpanel-react-native

Version:

Official React Native Tracking Library for Mixpanel Analytics

456 lines (371 loc) โ€ข 12.5 kB
# AsyncStorage Integration Patterns ## Overview The mixpanel-react-native library demonstrates sophisticated AsyncStorage integration patterns, including storage abstraction, graceful fallbacks, and efficient data serialization strategies for persistent analytics state. ## ๐Ÿ—๏ธ Storage Architecture ### Storage Abstraction Layer ```javascript // javascript/mixpanel-storage.js export class AsyncStorageAdapter { constructor(storage) { if (!storage) { try { // Dynamic import to handle missing dependency const storageModule = require("@react-native-async-storage/async-storage"); if (storageModule.default) { this.storage = storageModule.default } else { this.storage = storageModule } } catch { console.error( "[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null. " + "Please run 'npm install @react-native-async-storage/async-storage'" ); console.error("[Mixpanel] Falling back to in-memory storage"); this.storage = new InMemoryStorage(); } } else { this.storage = storage; } } } ``` **Abstraction Benefits**: - **Graceful degradation**: Falls back to in-memory storage - **Dependency injection**: Supports custom storage implementations - **Module compatibility**: Handles different import patterns - **Development flexibility**: Works without AsyncStorage in tests ### In-Memory Fallback Implementation ```javascript // javascript/mixpanel-storage.js class InMemoryStorage { constructor() { this.store = {}; } async getItem(key) { return this.store.hasOwnProperty(key) ? this.store[key] : null; } async setItem(key, value) { this.store[key] = value; } async removeItem(key) { delete this.store[key]; } } ``` **Fallback Strategy**: - **API consistency**: Same interface as AsyncStorage - **Session persistence**: Data survives within app session - **Test compatibility**: Perfect for unit testing - **Development workflow**: Works in environments without native storage ## ๐Ÿ”ง Error Handling Patterns ### Defensive Storage Operations ```javascript // javascript/mixpanel-storage.js export class AsyncStorageAdapter { async getItem(key) { try { return await this.storage.getItem(key); } catch { MixpanelLogger.error("error getting item from storage"); return null; // Always return null on failure } } async setItem(key, value) { try { await this.storage.setItem(key, value); } catch { MixpanelLogger.error("error setting item in storage"); // Silent failure - continue operation } } async removeItem(key) { try { await this.storage.removeItem(key); } catch { MixpanelLogger.error("error removing item from storage"); // Silent failure - continue operation } } } ``` **Error Handling Philosophy**: - **Silent failures**: Storage errors don't crash the app - **Logging**: All errors logged for debugging - **Fallback values**: Always return usable defaults - **Continue operation**: App functionality maintained despite storage issues ## ๐Ÿ—๏ธ Key Management Strategy ### Token-Scoped Key Generation ```javascript // javascript/mixpanel-constants.js export const getQueueKey = (token, type) => `MIXPANEL_${token}_${type}_QUEUE`; export const getDeviceIdKey = (token) => `MIXPANEL_${token}_DEVICE_ID`; export const getDistinctIdKey = (token) => `MIXPANEL_${token}_DISTINCT_ID`; export const getUserIdKey = (token) => `MIXPANEL_${token}_USER_ID`; export const getOptedOutKey = (token) => `MIXPANEL_${token}_OPT_OUT`; export const getSuperPropertiesKey = (token) => `MIXPANEL_${token}_SUPER_PROPERTIES`; export const getTimeEventsKey = (token) => `MIXPANEL_${token}_TIME_EVENTS`; export const getAppHasOpenedBeforeKey = (token) => `MIXPANEL_${token}_APP_HAS_OPENED_BEFORE`; ``` **Key Strategy Benefits**: - **Namespace isolation**: Prevents key collisions between projects - **Predictable naming**: Easy to debug and inspect - **Multi-tenant support**: Multiple Mixpanel projects per app - **Clear organization**: Keys grouped by functionality ### Key Lifecycle Management ```javascript // javascript/mixpanel-persistent.js async reset(token) { // Comprehensive cleanup of all token-related keys await this.storageAdapter.removeItem(getDeviceIdKey(token)); await this.storageAdapter.removeItem(getDistinctIdKey(token)); await this.storageAdapter.removeItem(getUserIdKey(token)); await this.storageAdapter.removeItem(getSuperPropertiesKey(token)); await this.storageAdapter.removeItem(getTimeEventsKey(token)); // Reload fresh state await this.loadIdentity(token); await this.loadSuperProperties(token); await this.loadTimeEvents(token); } ``` ## ๐Ÿ’พ Data Serialization Patterns ### JSON Serialization Strategy ```javascript // javascript/mixpanel-persistent.js // Serialization: Objects to strings async persistSuperProperties(token) { if (this._superProperties[token] === null) { return; } await this.storageAdapter.setItem( getSuperPropertiesKey(token), JSON.stringify(this._superProperties[token]) // Object โ†’ JSON string ); } // Deserialization: Strings to objects with fallbacks async loadSuperProperties(token) { const superPropertiesString = await this.storageAdapter.getItem( getSuperPropertiesKey(token) ); this._superProperties[token] = superPropertiesString ? JSON.parse(superPropertiesString) // JSON string โ†’ Object : {}; // Fallback to empty object } ``` **Serialization Principles**: - **JSON format**: Standard, debuggable, cross-platform - **Null safety**: Handle missing/corrupted data gracefully - **Default values**: Always provide sensible fallbacks - **Type consistency**: Maintain expected data types ### Complex Data Structure Handling ```javascript // javascript/mixpanel-persistent.js // Queue persistence (arrays of objects) async loadQueue(token, type) { const queueString = await this.storageAdapter.getItem( getQueueKey(token, type) ); return queueString ? JSON.parse(queueString) : []; // Default to empty array } async saveQueue(token, type, queue) { await this.storageAdapter.setItem( getQueueKey(token, type), JSON.stringify(queue) ); } // Boolean persistence (string conversion) async persistOptedOut(token) { if (this._optedOut[token] === null) { return; } await this.storageAdapter.setItem( getOptedOutKey(token), this._optedOut[token].toString() // Boolean โ†’ String ); } async loadOptOut(token) { const optOutString = await this.storageAdapter.getItem( getOptedOutKey(token) ); this._optedOut[token] = optOutString === "true"; // String โ†’ Boolean } ``` ## ๐Ÿš€ Performance Optimization Patterns ### Write-Through Caching ```javascript // javascript/mixpanel-persistent.js export class MixpanelPersistent { constructor(storageAdapter) { this.storageAdapter = storageAdapter; // In-memory caches for fast access this._superProperties = {}; this._timeEvents = {}; this._identity = {}; this._optedOut = {}; this._appHasOpenedBefore = {}; } // Read from cache (fast) getSuperProperties(token) { return this._superProperties[token]; } // Write to cache AND storage (write-through) updateSuperProperties(token, superProperties) { this._superProperties = { ...this._superProperties, [token]: { ...superProperties }, // Update cache }; // Note: persistSuperProperties() called separately for storage } } ``` **Caching Benefits**: - **Fast reads**: Memory access vs. async storage I/O - **Consistency**: Cache always reflects current state - **Batched writes**: Can optimize storage writes - **Reliability**: Storage failures don't affect memory state ### Lazy Loading Strategy ```javascript // javascript/mixpanel-persistent.js async loadDeviceId(token) { if (!token) { return; // Early exit for invalid token } const storageToken = await this.storageAdapter.getItem( getDeviceIdKey(token) ); if (!this._identity[token]) { this._identity[token] = {}; // Initialize token state only when needed } this._identity[token].deviceId = storageToken; if (!this._identity[token].deviceId) { // Generate only when missing try { this._identity[token].deviceId = randomUUID(); } catch (e) { this._identity[token].deviceId = uuid.v4(); } await this.storageAdapter.setItem( getDeviceIdKey(token), this._identity[token].deviceId ); } } ``` **Lazy Loading Benefits**: - **Memory efficiency**: Only load data when needed - **Startup performance**: Avoid loading unused token data - **Scalability**: Supports many tokens without memory bloat ## ๐Ÿ”„ Data Migration & Versioning ### Identity Generation with Fallbacks ```javascript // javascript/mixpanel-persistent.js async loadDeviceId(token) { // Load existing ID from storage const storageToken = await this.storageAdapter.getItem(getDeviceIdKey(token)); if (!this._identity[token]) { this._identity[token] = {}; } this._identity[token].deviceId = storageToken; if (!this._identity[token].deviceId) { // Generate new ID with fallback strategy try { this._identity[token].deviceId = randomUUID(); // Expo crypto (preferred) } catch (e) { this._identity[token].deviceId = uuid.v4(); // uuid package (fallback) } // Persist the new ID await this.storageAdapter.setItem( getDeviceIdKey(token), this._identity[token].deviceId ); } } async loadDistinctId(token) { const distinctId = await this.storageAdapter.getItem(getDistinctIdKey(token)); if (!this._identity[token]) { this._identity[token] = {}; } this._identity[token].distinctId = distinctId; if (!this._identity[token].distinctId) { // Generate device-based distinct ID this._identity[token].distinctId = "$device:" + this._identity[token].deviceId; await this.storageAdapter.setItem( getDistinctIdKey(token), this._identity[token].distinctId ); } } ``` ## ๐Ÿงช Testing Patterns ### AsyncStorage Mocking for Tests ```javascript // __tests__/jest_setup.js jest.mock("@react-native-async-storage/async-storage", () => ({ getItem: jest.fn().mockResolvedValue(null), setItem: jest.fn().mockResolvedValue(undefined), removeItem: jest.fn().mockResolvedValue(undefined), })); // __mocks__/@react-native-async-storage/async-storage.js export default { getItem: jest.fn((key) => { return new Promise((resolve) => { resolve(null); }); }), setItem: jest.fn((key, value) => { return new Promise((resolve) => { resolve(null); }); }), removeItem: jest.fn((key) => { return new Promise((resolve) => { resolve(null); }); }), }; ``` ### Storage Adapter Testing ```javascript // Example test pattern for storage adapter describe('AsyncStorageAdapter', () => { let adapter; let mockStorage; beforeEach(() => { mockStorage = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), }; adapter = new AsyncStorageAdapter(mockStorage); }); it('should handle storage errors gracefully', async () => { mockStorage.getItem.mockRejectedValue(new Error('Storage error')); const result = await adapter.getItem('test-key'); expect(result).toBeNull(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining('error getting item from storage') ); }); }); ``` ## ๐Ÿ“‹ AsyncStorage Best Practices Discovered ### 1. **Graceful Degradation** Always provide fallbacks when AsyncStorage is unavailable ### 2. **Error Resilience** Catch and handle all storage errors without crashing ### 3. **Key Namespacing** Use consistent, predictable key naming patterns ### 4. **Data Type Safety** Handle serialization/deserialization consistently ### 5. **Performance Optimization** Use write-through caching for frequently accessed data ### 6. **Memory Management** Implement lazy loading to avoid unnecessary memory usage ### 7. **Testing Strategy** Mock AsyncStorage completely for reliable unit tests ### 8. **Dependency Injection** Support custom storage implementations for flexibility ### 9. **Default Values** Always provide sensible defaults for missing data ### 10. **Logging Strategy** Log storage operations for debugging without exposing sensitive data