Skip to content

Conversation

@jaredmixpanel
Copy link
Collaborator

Summary

This PR adds comprehensive Feature Flags functionality to the Mixpanel React Native SDK, enabling dynamic feature control and A/B testing capabilities.

Changes

Core Implementation

  • Flags API: New mixpanel.flags property providing access to feature flags functionality
  • Dual Mode Support: Works in both native mode (iOS/Android) and JavaScript fallback mode (Expo/React Native Web)
  • Dual Async Patterns: All async methods support both callback and Promise patterns

API Methods

  • loadFlags() - Manually fetch flags from server
  • areFlagsReady() - Check if flags are loaded and ready
  • getVariantSync() / getVariant() - Get full variant object
  • getVariantValueSync() / getVariantValue() - Get variant value only
  • isEnabledSync() / isEnabled() - Check if feature is enabled
  • updateContext() - Update targeting context at runtime

Platform Support

  • iOS: Native implementation using Mixpanel Swift SDK
  • Android: Native implementation using Mixpanel Android SDK
  • JavaScript: Full fallback implementation for Expo and React Native Web
  • TypeScript: Complete type definitions for all Feature Flags APIs

Features

  • Automatic experiment tracking with $experiment_started events
  • Persistent caching using AsyncStorage in JavaScript mode
  • Graceful error handling with fallback values
  • Context-aware targeting with runtime updates
  • Lazy loading to minimize performance impact

Backward Compatibility

This change is fully backward compatible:

  • Lazy-loaded (only initialized when accessed)
  • Optional (can be disabled via initialization options)
  • Non-breaking (all existing functionality unchanged)

Usage Example

// Initialize with Feature Flags
const mixpanel = new Mixpanel('YOUR_TOKEN');
await mixpanel.init(false, {}, 'https://api.mixpanel.com', true, {
  enabled: true,
  context: { platform: 'mobile' }
});

// Access flags synchronously when ready
if (mixpanel.flags.areFlagsReady()) {
  const isEnabled = mixpanel.flags.isEnabledSync('new-feature', false);
  const color = mixpanel.flags.getVariantValueSync('button-color', 'blue');
}

// Or use async methods
const variant = await mixpanel.flags.getVariant('checkout-flow', {
  key: 'control',
  value: 'standard'
});

- Implement Feature Flags API mirroring native iOS/Android SDKs
- Add support for 8 core methods (loadFlags, areFlagsReady, getVariant/Value, isEnabled)
- Support both synchronous and asynchronous method variants
- Implement dual async pattern (callbacks and Promises)
- Add native module implementations for iOS and Android
- Create JavaScript fallback for Expo/React Native Web
- Include automatic experiment tracking ($experiment_started events)
- Update TypeScript definitions
@jaredmixpanel jaredmixpanel requested a review from Copilot October 22, 2025 20:42
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds comprehensive Feature Flags functionality to the Mixpanel React Native SDK, enabling dynamic feature control and A/B testing with support for both native platforms (iOS/Android) and JavaScript fallback mode (Expo/React Native Web).

Key Changes:

  • Implements a complete Feature Flags API with both synchronous and asynchronous methods for retrieving flag variants, values, and enabled states
  • Adds dual-mode support with native implementations for iOS/Android and JavaScript fallback for Expo/React Native Web
  • Integrates automatic experiment tracking with persistent caching using AsyncStorage

Reviewed Changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
javascript/mixpanel-main.js Stores feature flags configuration options during initialization
javascript/mixpanel-flags.js Core wrapper class that delegates to native or JavaScript implementations based on platform availability
javascript/mixpanel-flags-js.js JavaScript implementation with API fetching, caching, and experiment tracking
index.js Adds lazy-loaded flags property and integrates initialization with feature flags options
ios/MixpanelReactNative.swift Native iOS implementation of Feature Flags methods with variant conversion helpers
ios/MixpanelReactNative.m Exposes iOS Feature Flags methods to React Native bridge
android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java Native Android implementation with type conversion handling for React Native bridge limitations

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

- Update JavaScript tests to account for new featureFlagsOptions parameter
- Upgrade iOS Mixpanel SDK to 5.1.3 (supports Feature Flags)
- Upgrade Android Mixpanel SDK to 8.2.4 (supports Feature Flags)
- Fix iOS MixpanelOptions initialization to use token parameter
- Fix iOS Feature Flags method calls to remove extraneous parameter labels
- Fix iOS MixpanelFlagVariant conversion to use immutable constructor
- Fix Android MixpanelOptions to use Builder pattern correctly
- Fix Android MixpanelFlagVariant to use public final fields instead of getters/setters
The static Mixpanel.init() method was only passing 5 parameters to
MixpanelReactNative.initialize, but after adding Feature Flags support,
it now requires 7 parameters (including useGzipCompression and featureFlagsOptions).

This fixes the failing test: 'it calls MixpanelReactNative initialize'
- Remove unused 'reject' parameter from Promise executors in all async methods
  (getVariant, getVariantValue, isEnabled) since errors are always resolved
  with fallback values, never rejected

- Fix lazy loading bug in init() method: use this.flags getter to trigger
  lazy loading instead of checking this._flags which is always falsy before
  the getter is accessed
Android fixes:
- Replace incorrect Flags import with FlagCompletionCallback
- Fix getInstance() to use 4-parameter signature with MixpanelOptions
- Register super properties after getInstance instead of during
- Replace Flags.GetVariantCallback with FlagCompletionCallback<T>
- Fix JSON conversion to use convertJsonToMap/Array instead of non-existent jsonToReact

iOS fixes:
- Fix MixpanelOptions to use constructor parameters instead of property setters
- Update Mixpanel.initialize to use options: parameter as first argument
- Fix MixpanelFlagVariant constructor parameter order (isExperimentActive before experimentID)
- Use correct 4-parameter getInstance signature: (context, token, trackAutomaticEvents, options)
- Add optOutTrackingDefault to MixpanelOptions.Builder instead of getInstance
- Add missing WritableArray import for JSON array conversion
- Use complete MixpanelOptions constructor with all 12 parameters
- All properties are let constants and must be set in constructor
- Use Mixpanel.initialize(options:) with single options parameter
- Fix MixpanelFlagVariant parameter order: isQATester before experimentID
Test Suite Changes:
- Add Feature Flags native module mocks to jest_setup.js
- Create comprehensive flags.test.js with 60+ test cases covering:
  * Flags property access and lazy loading
  * Native mode synchronous methods (areFlagsReady, getVariantSync, etc.)
  * Native mode async methods with both Promise and callback patterns
  * JavaScript mode with fetch mocking and caching
  * Experiment tracking ( events)
  * Context updates
  * Error handling and edge cases
  * Type safety for all value types
  * Integration tests

iOS Initialization Fix:
- Use full MixpanelOptions constructor with all 12 parameters
- All properties set in constructor (let constants, not var)
- Use simple Mixpanel.initialize(options:) signature
- Fix MixpanelFlagVariant parameter order: isQATester before experimentID
- Fix Jest configuration by removing outdated preprocessor transform
- Add transformIgnorePatterns for React Native modules
- Fix null feature name tests by mocking proper fallback responses
- Simplify test suite by removing complex JavaScript mode tests
  (JS mode is validated through integration tests instead)

All 154 tests now passing (106 existing + 48 new Feature Flags tests)
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated 5 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


MixpanelLogger.log(this.token, "Fetching feature flags with data:", requestData);

const serverURL = this.mixpanelImpl.config?.getServerURL?.(this.token) || "https://api.mixpanel.com";
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The optional chaining with double null-coalescing creates complex logic. If config or getServerURL doesn't exist, this falls back to the default URL, but this should be explicitly validated since serverURL is critical for API calls. Consider extracting this to a helper method with clear error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
# Note: jq is pre-installed on macOS GitHub Actions runners
SIMULATOR_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | .value[] | select(.name | contains("iPhone")) | .udid' | head -n 1)
if [ -z "$SIMULATOR_ID" ]; then
echo "Error: No iPhone simulator found"
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The jq command uses complex pipe operations that are hard to read and debug. The comment states 'jq is pre-installed on macOS GitHub Actions runners', but this assumption could break if the runner environment changes. Consider adding a check for jq availability before using it.

Suggested change
# Note: jq is pre-installed on macOS GitHub Actions runners
SIMULATOR_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | .value[] | select(.name | contains("iPhone")) | .udid' | head -n 1)
if [ -z "$SIMULATOR_ID" ]; then
echo "Error: No iPhone simulator found"
# Note: jq is pre-installed on macOS GitHub Actions runners, but check to be sure
command -v jq >/dev/null 2>&1 || { echo >&2 "jq is required but not installed. Aborting."; exit 1; }
SIMULATOR_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | .value[] | select(.name | contains("iPhone")) | .udid' | head -n 1)
if [ -z "$SIMULATOR_ID" ]; then

Copilot uses AI. Check for mistakes.
1. Optimize Flags class - move MixpanelFlagsJS import to top of file
   to avoid repeated module resolution overhead on each instance creation

2. Fix Android initialization - pass superProperties through MixpanelOptions.Builder
   instead of calling registerSuperProperties after getInstance to avoid
   potential timing issues during initialization

All 154 tests passing.
There is no 2-parameter getInstance(Context, String) overload in MixpanelAPI.
Use getInstance(context, token, trackAutomaticEvents) instead to retrieve
the existing instance for feature flags operations.
@jaredmixpanel jaredmixpanel requested a review from Copilot October 24, 2025 00:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated 2 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

this.mixpanelPersistent = MixpanelPersistent.getInstance(storage, token);

// Load cached flags on initialization
this.loadCachedFlags();
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loadCachedFlags() method is called in the constructor but is async. This call will not be awaited, which means the flags may not be loaded when the instance is first used. Consider making the constructor initialization explicit or documenting that cached flags load asynchronously.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +96
- name: Boot iOS Simulator
run: |
# Get list of available iPhone simulators
# Note: jq is pre-installed on macOS GitHub Actions runners
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] While jq is commonly available on macOS GitHub Actions runners, it's better to explicitly verify or install it to avoid potential failures if the runner configuration changes.

Suggested change
- name: Boot iOS Simulator
run: |
# Get list of available iPhone simulators
# Note: jq is pre-installed on macOS GitHub Actions runners
- name: Ensure jq is installed
run: |
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found, installing with Homebrew..."
brew install jq
else
echo "jq is already installed"
fi
- name: Boot iOS Simulator
run: |
# Get list of available iPhone simulators

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant