mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
339 lines (295 loc) ⢠15.4 kB
Markdown
# System Design: mixpanel-react-native
## Architectural Overview
The Mixpanel React Native library implements a **dual-path architecture** with sophisticated queue management, persistent state, and graceful fallback mechanisms. The system prioritizes data reliability over performance, ensuring no analytics events are lost.
## šļø High-Level Architecture
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Application Layer ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Mixpanel ā ā People ā ā MixpanelGroup ā ā
ā ā (Events) ā ā (Profiles) ā ā (Group Analytics) ā ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Public API (index.js)
āāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Implementation Router ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Native Mode ā ā JavaScript Mode ā ā
ā ā (iOS Swift + ā OR ā (Pure JS Implementation) ā ā
ā ā Android Java) ā ā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Shared Infrastructure ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Queue Mgmt ā ā Persistence ā ā Network ā ā
ā ā (In-Memory ā ā (Storage ā ā (HTTP + Retry) ā ā
ā ā + Durable) ā ā Abstraction)ā ā ā ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
## š Data Flow Architecture
### 1. Event Tracking Flow
```
User Code
ā mixpanel.track("Event", {props})
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Input Validation Layer ā
ā ⢠Parameter validation (StringHelper) ā
ā ⢠Type checking (ObjectHelper) ā
ā ⢠Error throwing with helpful messages ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā Valid input
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Implementation Router ā
ā if (useNative && MixpanelReactNative) { ā
ā ā Native Implementation ā
ā } else { ā
ā ā JavaScript Implementation ā
ā } ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Event Enrichment ā
ā ⢠Add metadata (device info, lib version) ā
ā ⢠Merge super properties ā
ā ⢠Add identity fields (distinct_id, etc) ā
ā ⢠Calculate event timing if applicable ā
ā ⢠Add session metadata ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Enriched event object
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Opt-out Check ā
ā if (persistent.getOptedOut(token)) { ā
ā ā Log skip message and return ā
ā } ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā User opted in
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Queue Management ā
ā ⢠Add to in-memory queue ā
ā ⢠Persist to storage (AsyncStorage) ā
ā ⢠Trigger batch processing if needed ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Batch Processing ā
ā ⢠Collect events in batches (default: 50) ā
ā ⢠Process every 10s (JS) or 60s (native) ā
ā ⢠Handle network requests with retries ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā HTTP POST to Mixpanel API
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Mixpanel Servers ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
### 2. Identity Management Flow
```
User Action: mixpanel.identify("user123")
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Validation Layer ā
ā StringHelper.isValid(distinctId) ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā Valid string
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Persistence Layer Update ā
ā ⢠Update in-memory identity cache ā
ā ⢠Persist to AsyncStorage ā
ā ⢠Set userId (for People Analytics) ā
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Queue Identity Update ā
ā ⢠Update existing USER queue records ā
ā ⢠Ensure consistent identity fields ā
ā ⢠Minimize object copying (performance) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
## šļø System Layers
### Layer 1: Public API (index.js)
**Responsibility**: Type-safe interface with comprehensive validation
```javascript
// Clean, typed interface with validation
export class Mixpanel {
track(eventName, properties) {
if (!StringHelper.isValid(eventName)) {
StringHelper.raiseError(PARAMS.EVENT_NAME);
}
// Route to implementation...
}
}
```
**Key Characteristics**:
- Input validation with helpful error messages
- Implementation routing (native vs JS)
- Promise-based API for async operations
- TypeScript definitions for developer experience
### Layer 2: Implementation Layer
**Responsibility**: Platform-specific optimization
#### Native Path (iOS Swift + Android Java)
- Direct integration with official Mixpanel SDKs
- Platform-optimized performance
- Native queue management (60s flush interval)
- iOS: Background flushing support
#### JavaScript Path (mixpanel-main.js)
- Pure JavaScript implementation
- Expo and React Native Web compatibility
- Aggressive flushing (10s interval)
- Comprehensive retry logic
### Layer 3: Core Processing (mixpanel-core.js)
**Responsibility**: Shared business logic
```javascript
export const MixpanelCore = (storage) => {
// Queue processing with batching
const processQueue = async (token, type) => {
const batch = queue.slice(0, batchSize);
try {
await MixpanelNetwork.sendRequest({...});
// Remove successful items
} catch (error) {
handleBatchError(token, error, type, processBatch);
}
};
};
```
**Key Features**:
- Event queue management with batching
- Automatic retry with exponential backoff
- Corrupt data detection and removal
- Session metadata injection
### Layer 4: Persistence Layer (mixpanel-persistent.js)
**Responsibility**: State management across app sessions
```javascript
export class MixpanelPersistent {
// Token-scoped state management
constructor(storageAdapter) {
this._identity = {}; // distinctId, deviceId, userId per token
this._superProperties = {}; // Global event properties per token
this._timeEvents = {}; // Event timing per token
this._optedOut = {}; // Opt-out status per token
}
}
```
**Storage Strategy**:
- Token-scoped keys: `MIXPANEL_{token}_{type}_{field}`
- Lazy loading on first access
- Write-through caching (memory + storage)
- Graceful failure with in-memory fallback
### Layer 5: Infrastructure Layer
#### Queue Management (mixpanel-queue.js)
```javascript
export const MixpanelQueueManager = (() => {
let _queues = {}; // In-memory queues per token
const enqueue = async (token, type, data) => {
_queues[token][type].push(data);
await updateQueueInStorage(token, type); // Persist immediately
};
})();
```
**Queue Types**:
- `EVENTS`: `/track/` - Analytics events
- `USER`: `/engage/` - People profile updates
- `GROUPS`: `/groups/` - Group analytics updates
#### Network Layer (mixpanel-network.js)
```javascript
export const MixpanelNetwork = (() => {
const sendRequest = async ({...params, retryCount = 0}) => {
try {
const response = await fetch(url, {...});
// Handle response...
} catch (error) {
if (retryCount < maxRetries) {
const backoff = Math.min(2 ** retryCount * 2000, 60000);
await new Promise(resolve => setTimeout(resolve, backoff));
return sendRequest({...params, retryCount: retryCount + 1});
}
}
};
})();
```
**Retry Strategy**:
- Exponential backoff: 2s, 4s, 8s, 16s, 32s, 60s (max)
- Max 5 retries before giving up
- 400 errors bypass retry (corrupted data)
#### Storage Abstraction (mixpanel-storage.js)
```javascript
export class AsyncStorageAdapter {
constructor(storage) {
// Try AsyncStorage, fall back to in-memory
if (!storage) {
try {
this.storage = require("@react-native-async-storage/async-storage");
} catch {
this.storage = new InMemoryStorage();
}
}
}
}
```
## š§ Configuration Management
### Token-Scoped Configuration (mixpanel-config.js)
```javascript
export class MixpanelConfig {
static getInstance() { /* Singleton */ }
// All configuration is per-token
setFlushBatchSize(token, batchSize) {
this._config[token] = {...this._config[token], batchSize};
}
}
```
**Configuration Hierarchy**:
1. User-provided values (highest priority)
2. Default constants from mixpanel-constants.js
3. Fallback values in getters
## šÆ Design Decisions & Trade-offs
### 1. Reliability Over Performance
**Decision**: Persist every event immediately to storage
**Trade-off**: Storage I/O overhead vs. zero data loss
**Justification**: Analytics data is business-critical
### 2. Token-Based Multi-Tenancy
**Decision**: Support multiple Mixpanel projects per app
**Trade-off**: Memory overhead vs. flexibility
**Justification**: Enterprise customers need project isolation
### 3. Dual Implementation Strategy
**Decision**: Maintain both native and JS implementations
**Trade-off**: Code duplication vs. platform compatibility
**Justification**: Expo support essential for adoption
### 4. Aggressive Error Handling
**Decision**: Validate all inputs, handle all errors
**Trade-off**: Code verbosity vs. developer experience
**Justification**: SDK failures should never crash apps
### 5. Queue-Based Architecture
**Decision**: Async event processing with batching
**Trade-off**: Complexity vs. performance and reliability
**Justification**: Network efficiency and offline support
## š Performance Characteristics
### Memory Usage
- In-memory queues per token/type
- LRU-style identity caching
- Minimal object allocation during normal operation
### Network Efficiency
- Batched requests (50 events default)
- Compression support (gzip)
- Intelligent retry with backoff
### Storage I/O
- Write-through cache for all persistent data
- JSON serialization for complex objects
- Graceful fallback to in-memory storage
### CPU Usage
- Lazy initialization of expensive operations
- Efficient queue processing with splice operations
- Minimal validation overhead with helper classes