UNPKG

mixpanel-react-native

Version:

Official React Native Tracking Library for Mixpanel Analytics

380 lines (289 loc) โ€ข 10.5 kB
# Discovered Patterns: mixpanel-react-native ## Code Architecture Philosophy This codebase demonstrates **graceful degradation** and **defensive programming** principles. The team prioritizes reliability over performance, with comprehensive fallback mechanisms and robust error handling throughout. ## ๐Ÿ—๏ธ Architectural Patterns ### 1. Singleton Pattern with Lazy Initialization **Pattern**: Factory functions returning singleton instances for shared resources. ```javascript // javascript/mixpanel-queue.js export const MixpanelQueueManager = (() => { let _queues = {}; let mixpanelPersistent; const getPersistent = () => { if (!mixpanelPersistent) { mixpanelPersistent = MixpanelPersistent.getInstance(); } return mixpanelPersistent; }; // ... })(); ``` **Motivation**: Ensures single source of truth for queue state and persistent storage while avoiding initialization costs until needed. ### 2. Factory Function Pattern for Dependency Injection **Pattern**: Core modules accept dependencies via factory functions rather than direct imports. ```javascript // javascript/mixpanel-core.js export const MixpanelCore = (storage) => { const mixpanelPersistent = MixpanelPersistent.getInstance(storage); // ... return { initialize, startProcessingQueue, addToMixpanelQueue, flush, identifyUserQueue, }; }; ``` **Motivation**: Enables testing with mock storage and supports different storage implementations (AsyncStorage vs. custom). ### 3. Token-Based Multi-Tenancy **Pattern**: All operations include token parameter for instance isolation. ```javascript // Every function signature includes token as first parameter const flush = async (token) => { /* ... */ }; const track = async (token, eventName, properties) => { /* ... */ }; // Storage keys are token-scoped export const getQueueKey = (token, type) => `MIXPANEL_${token}_${type}_QUEUE`; ``` **Motivation**: Supports multiple Mixpanel projects within single app without data leakage. ## ๐ŸŽฏ Naming Conventions ### File Naming - **Kebab-case**: `mixpanel-core.js`, `mixpanel-queue.js` - **Descriptive modules**: Each file represents a single responsibility - **Prefix consistency**: All JavaScript modules start with `mixpanel-` ### Variable Naming - **camelCase**: Standard JavaScript convention (`mixpanelImpl`, `trackAutomaticEvents`) - **Constants**: SCREAMING_SNAKE_CASE (`DEFAULT_OPT_OUT`, `PARAMS.TOKEN`) - **Private variables**: Leading underscore (`_queues`, `_shouldLog`) ### Function Naming - **Verb-first**: `initialize`, `track`, `flush`, `addToMixpanelQueue` - **Boolean predicates**: `isValidAndSerializable`, `hasOptedOutTracking` - **Event handlers**: `handleBatchError` ## ๐Ÿ“ฆ Import/Export Patterns ### Import Organization ```javascript // External dependencies first import { Platform } from "react-native"; import packageJson from "./package.json"; // Internal modules second, grouped by purpose import { MixpanelCore } from "./mixpanel-core"; import { MixpanelType } from "./mixpanel-constants"; import { MixpanelConfig } from "./mixpanel-config"; ``` ### Export Strategies - **Named exports for utilities**: `export class MixpanelLogger` - **Default exports for main classes**: `export default class MixpanelMain` - **Factory function exports**: `export const MixpanelCore = (storage) => {...}` ## ๐Ÿ›ก๏ธ Error Handling Philosophy ### 1. Comprehensive Input Validation **Pattern**: Helper classes for parameter validation with descriptive errors. ```javascript // index.js class StringHelper { static isValid(str) { return typeof str === "string" && !/^\s*$/.test(str); } static raiseError(paramName) { throw new Error(`${paramName}${ERROR_MESSAGE.INVALID_STRING}`); } } // Usage if (!StringHelper.isValid(distinctId)) { StringHelper.raiseError(PARAMS.DISTINCT_ID); } ``` ### 2. Graceful Degradation **Pattern**: Try native implementation, fall back to JavaScript mode. ```javascript // index.js if (useNative && MixpanelReactNative) { this.mixpanelImpl = MixpanelReactNative; return; } else if (useNative) { console.warn("MixpanelReactNative is not available; using JavaScript mode..."); } this.mixpanelImpl = new MixpanelMain(token, trackAutomaticEvents, storage); ``` ### 3. Safe Storage Operations **Pattern**: Wrap all storage operations in try-catch with fallbacks. ```javascript // javascript/mixpanel-storage.js async getItem(key) { try { return await this.storage.getItem(key); } catch { MixpanelLogger.error("error getting item from storage"); return null; } } ``` #### Evolution [Updated: 2025-05-30] Storage module resolution now handles both ES6 and CommonJS exports: ```javascript const storageModule = require("@react-native-async-storage/async-storage"); if (storageModule.default) { this.storage = storageModule.default } else { this.storage = storageModule } ``` ### 4. UUID Generation with Expo Compatibility [Updated: 2025-05-30] **Pattern**: Try platform-specific method first, fallback to universal. ```javascript // javascript/mixpanel-persistent.js try { this._identity[token].deviceId = randomUUID(); // expo-crypto } catch (e) { this._identity[token].deviceId = uuid.v4(); // uuid package } ``` **Motivation**: Better Expo support while maintaining backward compatibility. ## ๐Ÿ”„ Async Patterns ### 1. Promise-Based API with Error Propagation **Pattern**: Native methods return Promises, JavaScript methods async/await. ```javascript // Native (iOS Swift) @objc func initialize(_ token: String, /* ... */) -> Void { // ... resolve(true) } // JavaScript wrapper identify(distinctId) { return new Promise((resolve, reject) => { if (!StringHelper.isValid(distinctId)) { reject(new Error("Invalid distinctId")); } this.mixpanelImpl.identify(this.token, distinctId) .then(() => resolve()) .catch((err) => reject(err)); }); } ``` ### 2. Retry Logic with Exponential Backoff **Pattern**: Network requests implement sophisticated retry strategy. ```javascript // javascript/mixpanel-network.js const maxRetries = 5; const backoff = Math.min(2 ** retryCount * 2000, 60000); // Exponential backoff if (retryCount < maxRetries) { MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, backoff)); return sendRequest({ /* ... */, retryCount: retryCount + 1 }); } ``` ## ๐Ÿงช Testing Patterns ### 1. Comprehensive Mocking Strategy **Pattern**: Mock all external dependencies at the module level. ```javascript // __tests__/jest_setup.js jest.mock("expo-crypto", () => ({ randomUUID: jest.fn(() => "mocked-uuid-string-" + Math.random().toString(36).substring(2, 15)) })); jest.mock("@react-native-async-storage/async-storage", () => ({ getItem: jest.fn().mockResolvedValue(null), setItem: jest.fn().mockResolvedValue(undefined), removeItem: jest.fn().mockResolvedValue(undefined), })); ``` ### 2. Test File Organization **Pattern**: Mirror source structure with `.test.js` suffix. ``` __tests__/ โ”œโ”€โ”€ core.test.js # Tests javascript/mixpanel-core.js โ”œโ”€โ”€ network.test.js # Tests javascript/mixpanel-network.js โ”œโ”€โ”€ queue.test.js # Tests javascript/mixpanel-queue.js โ””โ”€โ”€ jest_setup.js # Global test configuration ``` ## ๐Ÿ“ Documentation Patterns ### 1. JSDoc with Rich Examples **Pattern**: Comprehensive JSDoc comments with usage examples. ```javascript /** * Associate all future calls to track() with the user identified by * the given distinct id. * * @param {string} distinctId a string uniquely identifying this user * @returns {Promise} A promise that resolves when the identify is successful */ identify(distinctId) { /* ... */ } ``` ### 2. Inline Comments for Complex Logic **Pattern**: Explain "why" not "what" in comments. ```javascript // Only update if there is a difference; minimize object copies if (updateDistinctId || updateDeviceId || updateUserId) { updated = { ...record }; // shallow copy only if needed // ... } ``` ## ๐Ÿ” Logging Philosophy ### 1. Token-Scoped Conditional Logging **Pattern**: All logs include token context and respect per-instance settings. ```javascript // javascript/mixpanel-logger.js static log(token, ...args) { if (MixpanelLogger._shouldLog(token)) { console.log(...MixpanelLogger._prependPrefix(args)); } } // Usage with context MixpanelLogger.log(token, `Track '${eventName}' with properties`, properties); ``` ### 2. Structured Logging with Prefixes **Pattern**: Consistent log format for easy filtering. ```javascript static _prependPrefix(args) { return ["[Mixpanel]", ...args]; } // Output: [Mixpanel] Track 'Plan Selected' with properties {...} ``` ## ๐Ÿš€ Performance Patterns ### 1. Batched Processing with Queues **Pattern**: Collect events in memory, flush in configurable batches. ```javascript // Process in batches to reduce network overhead const batchSize = config.getFlushBatchSize(token); const batch = queue.slice(0, batchSize); // Recursive processing for large queues if (queue.length > 0) { setTimeout(processBatch, 0); // Non-blocking recursion } ``` ### 2. Immutable State Updates **Pattern**: Use spread operator for shallow copies, minimize object creation. ```javascript // Only create new objects when necessary const eventProperties = Object.freeze({ token, time: Date.now(), ...this.getMetaData(), ...superProperties, ...properties, ...identityProps, }); ``` ### 3. Conditional Property Inclusion [Updated: 2025-05-30] **Pattern**: Use conditional spread to exclude null/undefined values. ```javascript // Prevents sending null values to API const profileData = { $token: token, $time: Date.now(), ...action, ...(distinctId != null && { $distinct_id: distinctId }), ...(deviceId != null && { $device_id: deviceId }), ...(userId != null && { $user_id: userId }), }; ``` **Motivation**: Cleaner API payloads and prevents backend issues with null values. ## ๐ŸŽจ Code Style Insights ### Philosophy - **Defensive coding**: Assume everything can fail - **Explicit over implicit**: Clear parameter names and validation - **Fail fast**: Validate inputs immediately - **Progressive enhancement**: Native performance with JS fallback ### Trade-offs Made - **Reliability over performance**: Multiple validation layers - **Developer experience over bundle size**: Helpful error messages - **Compatibility over simplicity**: Support both native and JS modes