UNPKG

mixpanel-react-native

Version:

Official React Native Tracking Library for Mixpanel Analytics

404 lines (334 loc) 11.3 kB
# Workflow: Adding New Features ## Overview 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. ## 🎯 Feature Addition Workflow ### Step 1: Design the Public API ```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 ### Step 3: Implement Native Android Version ```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 ### Step 4: Implement JavaScript Fallback ```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'); }); }); }); ``` ### Step 6: Update Documentation ```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... } ``` ### Step 7: Update Native Module Mocks ```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); }); ``` ## 🔍 Integration Testing ### End-to-End Feature Test ```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 }) }) ); }); }); ``` ## 📱 Sample App Updates ### Update Sample Applications ```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> ); }; ``` ## 🚀 Release Considerations ### Version Compatibility ```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' }); } ``` #### Example: Gzip Compression Feature [Updated: 2025-05-30] 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); ``` ### Native Module Updates ```bash # iOS: After adding Swift methods cd ios && pod install # Android: After adding Java methods cd android && ./gradlew clean && ./gradlew build ``` ## ✅ Feature Checklist ### Pre-Implementation - [ ] Design API following existing patterns - [ ] Validate feature fits dual-implementation strategy - [ ] Check compatibility with both native SDKs - [ ] Plan fallback strategy for JavaScript mode ### Implementation - [ ] Add TypeScript definitions - [ ] Implement public API with validation - [ ] Add iOS native implementation - [ ] Add Android native implementation - [ ] Implement JavaScript fallback - [ ] Add comprehensive logging ### Testing - [ ] 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 ### Documentation & Samples - [ ] Add JSDoc documentation with examples - [ ] Update sample applications - [ ] Test in both development and production - [ ] Verify autolinking still works ### Release - [ ] Test iOS pod install process - [ ] Test Android gradle build process - [ ] Verify backwards compatibility - [ ] Update CHANGELOG.md - [ ] Consider version bump requirements ## 🔧 Common Patterns ### Constants Management ```javascript // javascript/mixpanel-constants.js export const CUSTOM_DIMENSION_PREFIX = '$custom_dimension_'; ``` ### Error Handling ```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 } ``` ### Configuration Support ```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.