mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
404 lines (334 loc) • 11.3 kB
Markdown
This workflow demonstrates how to add new features to the mixpanel-react-native library following the established dual-implementation pattern and maintaining compatibility across both native and JavaScript modes.
```javascript
// 1. Update index.d.ts with TypeScript definitions
export class Mixpanel {
// Add new method signature
setCustomDimension(dimension: string, value: MixpanelType): void;
}
// 2. Add to main Mixpanel class in index.js
export class Mixpanel {
setCustomDimension(dimension, value) {
// Input validation using established pattern
if (!StringHelper.isValid(dimension)) {
StringHelper.raiseError("dimension");
}
if (!ObjectHelper.isValidOrUndefined(value)) {
ObjectHelper.raiseError("value");
}
// Route to implementation
this.mixpanelImpl.setCustomDimension(this.token, dimension, value);
}
}
```
**API Design Principles**:
- Follow existing naming conventions (camelCase)
- Include comprehensive input validation
- Use helper classes for consistent error messages
- Route through implementation layer
### Step 2: Implement Native iOS Version
```swift
// ios/MixpanelReactNative.swift
@objc
func setCustomDimension(_ token: String,
dimension: String,
value: Any,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) -> Void {
let instance = MixpanelReactNative.getMixpanelInstance(token)
guard let instance = instance else {
reject("Instance Error", "Failed to get Mixpanel instance", nil)
return
}
// Convert React Native value to native type
let processedValue = MixpanelTypeHandler.processProperty(value: value)
// Call native SDK method
instance.setCustomDimension(dimension: dimension, value: processedValue)
resolve(nil)
}
```
**Native Implementation Pattern**:
- Use `@objc` decorator for React Native bridge
- Follow Promise-based async pattern
- Include error handling for missing instances
- Use type conversion utilities
- Call through to official native SDK
```java
// android/.../MixpanelReactNativeModule.java
@ReactMethod
public void setCustomDimension(final String token,
String dimension,
Dynamic value,
Promise promise) throws JSONException {
MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
if (instance == null) {
promise.reject("Instance Error", "Failed to get Mixpanel instance");
return;
}
synchronized (instance) {
// Convert React Native Dynamic to appropriate type
Object processedValue = ReactNativeHelper.dynamicToObject(value);
// Call native SDK method
instance.setCustomDimension(dimension, processedValue);
promise.resolve(null);
}
}
```
**Android Implementation Pattern**:
- Use `@ReactMethod` annotation
- Include thread synchronization for safety
- Use helper utilities for type conversion
- Follow established error handling pattern
```javascript
// javascript/mixpanel-main.js
export default class MixpanelMain {
async setCustomDimension(token, dimension, value) {
if (this.mixpanelPersistent.getOptedOut(token)) {
MixpanelLogger.log(token, `User has opted out, skipping setCustomDimension`);
return;
}
MixpanelLogger.log(token, `Setting custom dimension '${dimension}' to '${value}'`);
// Store dimension in super properties for future events
const currentSuperProps = this.mixpanelPersistent.getSuperProperties(token) || {};
const updatedSuperProps = {
...currentSuperProps,
[`$custom_dimension_${dimension}`]: value
};
await this.registerSuperProperties(token, updatedSuperProps);
}
}
```
**JavaScript Implementation Pattern**:
- Check opt-out status first
- Add comprehensive logging
- Use existing infrastructure (super properties)
- Follow async/await pattern
### Step 5: Add Comprehensive Tests
```javascript
// __tests__/custom-dimension.test.js
describe('Custom Dimensions', () => {
let mixpanel;
beforeEach(() => {
jest.clearAllMocks();
mixpanel = new Mixpanel('test-token', true);
});
describe('Input Validation', () => {
it('should throw error for invalid dimension', () => {
expect(() => {
mixpanel.setCustomDimension('', 'value');
}).toThrow('dimension is not a valid string');
});
it('should throw error for invalid value', () => {
expect(() => {
mixpanel.setCustomDimension('test', Symbol('invalid'));
}).toThrow('value is not a valid json object');
});
});
describe('Native Mode', () => {
it('should call native implementation', () => {
mixpanel.setCustomDimension('user_segment', 'premium');
expect(MixpanelReactNative.setCustomDimension).toHaveBeenCalledWith(
'test-token',
'user_segment',
'premium'
);
});
});
describe('JavaScript Mode', () => {
beforeEach(() => {
// Force JavaScript mode
mixpanel = new Mixpanel('test-token', true, false);
});
it('should store as super property', async () => {
await mixpanel.setCustomDimension('user_segment', 'premium');
const superProps = await mixpanel.getSuperProperties();
expect(superProps).toEqual({
'$custom_dimension_user_segment': 'premium'
});
});
it('should respect opt-out status', async () => {
await mixpanel.optOutTracking();
await mixpanel.setCustomDimension('test', 'value');
// Should not modify super properties when opted out
const superProps = await mixpanel.getSuperProperties();
expect(superProps).not.toHaveProperty('$custom_dimension_test');
});
});
});
```
```javascript
// Update JSDoc comments
/**
* Set a custom dimension that will be included with all future events.
* Custom dimensions allow you to segment your data by user characteristics
* or behaviors that are important to your analysis.
*
* @param {string} dimension The name of the custom dimension
* @param {object} value The value to associate with this dimension
*
* @example
* // Set user segment dimension
* mixpanel.setCustomDimension('user_segment', 'premium');
*
* // Set numeric dimension
* mixpanel.setCustomDimension('account_age_days', 45);
*/
setCustomDimension(dimension, value) {
// Implementation...
}
```
```javascript
// __tests__/jest_setup.js
jest.doMock("react-native", () => {
return Object.setPrototypeOf({
NativeModules: {
MixpanelReactNative: {
// ... existing methods
setCustomDimension: jest.fn(), // Add new method to mock
},
},
}, ReactNative);
});
```
```javascript
// __tests__/integration/custom-dimension.integration.test.js
describe('Custom Dimension Integration', () => {
it('should include custom dimension in tracked events', async () => {
const mixpanel = new Mixpanel('test-token', true);
await mixpanel.init();
// Set custom dimension
await mixpanel.setCustomDimension('user_tier', 'premium');
// Track event
mixpanel.track('Purchase', { amount: 99.99 });
// Verify dimension included in event
expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
'test-token',
MixpanelType.EVENTS,
expect.objectContaining({
event: 'Purchase',
properties: expect.objectContaining({
'$custom_dimension_user_tier': 'premium',
amount: 99.99
})
})
);
});
});
```
```javascript
// Samples/SimpleMixpanel/App.tsx
const SampleApp = () => {
const handleSetDimension = () => {
mixpanel.setCustomDimension('user_segment', 'mobile_user');
};
return (
<SafeAreaView>
<Button
title="Set Custom Dimension"
onPress={handleSetDimension}
/>
</SafeAreaView>
);
};
```
```javascript
// Consider backwards compatibility
// New features should not break existing implementations
// Add feature detection if needed:
if (mixpanel.setCustomDimension) {
mixpanel.setCustomDimension('feature', 'enabled');
} else {
// Fallback for older versions
mixpanel.registerSuperProperties({
'$custom_dimension_feature': 'enabled'
});
}
```
Added as optional parameter to maintain backward compatibility:
```javascript
// Old API still works
await mixpanel.init(false, {}, "https://api.mixpanel.com");
// New API with compression
await mixpanel.init(false, {}, "https://api.mixpanel.com", true);
```
```bash
cd ios && pod install
cd android && ./gradlew clean && ./gradlew build
```
- [ ] Design API following existing patterns
- [ ] Validate feature fits dual-implementation strategy
- [ ] Check compatibility with both native SDKs
- [ ] Plan fallback strategy for JavaScript mode
- [ ] Add TypeScript definitions
- [ ] Implement public API with validation
- [ ] Add iOS native implementation
- [ ] Add Android native implementation
- [ ] Implement JavaScript fallback
- [ ] Add comprehensive logging
- [ ] Unit tests for input validation
- [ ] Unit tests for native mode
- [ ] Unit tests for JavaScript mode
- [ ] Integration tests for complete workflow
- [ ] Update native module mocks
- [ ] Test opt-out behavior
- [ ] Add JSDoc documentation with examples
- [ ] Update sample applications
- [ ] Test in both development and production
- [ ] Verify autolinking still works
- [ ] Test iOS pod install process
- [ ] Test Android gradle build process
- [ ] Verify backwards compatibility
- [ ] Update CHANGELOG.md
- [ ] Consider version bump requirements
```javascript
// javascript/mixpanel-constants.js
export const CUSTOM_DIMENSION_PREFIX = '$custom_dimension_';
```
```javascript
// Always include comprehensive error handling
try {
await this.setCustomDimension(token, dimension, value);
} catch (error) {
MixpanelLogger.error(token, `Failed to set custom dimension: ${error.message}`);
// Continue gracefully
}
```
```javascript
// javascript/mixpanel-config.js
export class MixpanelConfig {
setCustomDimensionPrefix(token, prefix) {
this._config[token] = {
...this._config[token],
customDimensionPrefix: prefix,
};
}
}
```
This workflow ensures new features maintain the library's reliability, performance, and compatibility standards while following established architectural patterns.