mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
380 lines (289 loc) โข 10.5 kB
Markdown
# 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)
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