Skip to content

feat(analytics): Adds feature flag support to main SDK - #434

Merged
efahk merged 77 commits into
masterfrom
feature/add-feature-flags-support
Jul 10, 2026
Merged

feat(analytics): Adds feature flag support to main SDK#434
efahk merged 77 commits into
masterfrom
feature/add-feature-flags-support

Conversation

@efahk

@efahk efahk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This change brings in the react native feature flag implementation into the main SDK

jaredmixpanel and others added 30 commits December 5, 2025 14:49
- 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
- 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)
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.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Change endpoint from /decide to /flags with query parameters
- Build context object with distinct_id, device_id, and custom context
- Add query params: context (JSON), token, mp_lib, $lib_version
- Use dynamic version from package.json instead of hardcoded value
- Set request data to null (params in query string)
@efahk
efahk requested review from a team and tylerjroach July 8, 2026 19:04
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

The core flag evaluation path is functional but has two issues that can cause silent data loss or incorrect fallback behavior in production.

The persistence layer unconditionally deletes stored flag variants when the resolved policy is NETWORK_ONLY — which includes the case where flags are entirely disabled. Any code path that accesses mixpanel.flags without enabled: true would wipe previously persisted data. The pending_variant null-access crash in _processFirstTimeEventCheck (flagged in a prior review cycle but not yet fixed) can also throw on every track() call matching first-time-event criteria if the server omits pending_variant.

javascript/mixpanel-flag-persistence.js (unconditional clear() on disabled-flags access) and javascript/mixpanel-flags-js.js (missing null guard around pendingEvent.pending_variant in _processFirstTimeEventCheck).

Important Files Changed

Filename Overview
javascript/mixpanel-flag-persistence.js Flag storage layer with TTL and distinct_id context validation. loadFlagsFromStorage unconditionally calls clear() on NETWORK_ONLY policy, which fires even when flags are entirely disabled, silently deleting persisted variants.
javascript/mixpanel-flags-js.js Core JS-mode flag implementation: network fetch with W3C traceparent, json-logic first-time-event matching, experiment tracking. Has the unaddressed pending_variant null-access crash when server omits the field.
index.js Public SDK surface: adds flags lazy getter, featureFlagsOptions plumbing, and flag reset/identify integration. Native mode auto-loads flags during init(); JS mode does not, creating an undocumented async asymmetry.
javascript/mixpanel-flags.js Thin dispatch layer routing flag calls to native or JS implementation; dual callback/Promise API. Logic looks correct.
javascript/mixpanel-network.js Refactored to support GET (flags) and POST (events); now forwards headers on retries, fixing the previously flagged Authorization header drop.
javascript/mixpanel-main.js Adds featureFlagsOptions storage and _flagsJS back-reference for identify/track integration. Logic is correct.
ios/MixpanelReactNative.swift Adds synchronous and async flag bridge methods for iOS. Synchronous methods correctly use RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD.
android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java Adds flag bridge methods for Android; all sync methods correctly annotated with isBlockingSynchronousMethod = true.
index.d.ts Adds TypeScript types for all new flag APIs. Types look complete and consistent with the implementation.
javascript/mixpanel-persistent.js Adds generateUUID helper with proper fallbacks and fixes initializationCompletePromise to return the Promise. Both changes are correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App
    participant Mixpanel
    participant Flags
    participant FlagsJS as MixpanelFlagsJS
    participant Network as MixpanelNetwork
    participant Storage as AsyncStorage

    App->>Mixpanel: "await init(opts, {enabled:true})"
    Mixpanel->>Mixpanel: store featureFlagsOptions
    Mixpanel->>Mixpanel: await mixpanelImpl.initialize()
    Note over Mixpanel: JS mode: skip flag load
    Note over Mixpanel: Native mode: await flags.loadFlags()

    App->>Mixpanel: mixpanel.flags (getter)
    Mixpanel->>Flags: new Flags(token, impl)
    Flags->>FlagsJS: new MixpanelFlagsJS(...)
    Flags->>FlagsJS: init() [not awaited]
    FlagsJS->>Storage: loadFlagsFromStorage(context)
    Storage-->>FlagsJS: persisted variants or null
    FlagsJS->>Network: "GET /flags?context=..."
    Network-->>FlagsJS: flags + pending_first_time_events
    FlagsJS->>Storage: save(context, flags)

    App->>Flags: await getVariant(flag, fallback)
    Flags->>FlagsJS: getVariant(...)
    FlagsJS->>FlagsJS: await persistenceLoadedPromise
    FlagsJS->>FlagsJS: await fetchPromise
    FlagsJS-->>App: variant

    App->>Mixpanel: track(Purchase, props)
    Mixpanel->>FlagsJS: checkFirstTimeEvents(Purchase, props)
    FlagsJS->>FlagsJS: match pendingFirstTimeEvents
    FlagsJS->>Network: "POST /flags/{id}/first-time-events"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App
    participant Mixpanel
    participant Flags
    participant FlagsJS as MixpanelFlagsJS
    participant Network as MixpanelNetwork
    participant Storage as AsyncStorage

    App->>Mixpanel: "await init(opts, {enabled:true})"
    Mixpanel->>Mixpanel: store featureFlagsOptions
    Mixpanel->>Mixpanel: await mixpanelImpl.initialize()
    Note over Mixpanel: JS mode: skip flag load
    Note over Mixpanel: Native mode: await flags.loadFlags()

    App->>Mixpanel: mixpanel.flags (getter)
    Mixpanel->>Flags: new Flags(token, impl)
    Flags->>FlagsJS: new MixpanelFlagsJS(...)
    Flags->>FlagsJS: init() [not awaited]
    FlagsJS->>Storage: loadFlagsFromStorage(context)
    Storage-->>FlagsJS: persisted variants or null
    FlagsJS->>Network: "GET /flags?context=..."
    Network-->>FlagsJS: flags + pending_first_time_events
    FlagsJS->>Storage: save(context, flags)

    App->>Flags: await getVariant(flag, fallback)
    Flags->>FlagsJS: getVariant(...)
    FlagsJS->>FlagsJS: await persistenceLoadedPromise
    FlagsJS->>FlagsJS: await fetchPromise
    FlagsJS-->>App: variant

    App->>Mixpanel: track(Purchase, props)
    Mixpanel->>FlagsJS: checkFirstTimeEvents(Purchase, props)
    FlagsJS->>FlagsJS: match pendingFirstTimeEvents
    FlagsJS->>Network: "POST /flags/{id}/first-time-events"
Loading

Reviews (10): Last reviewed commit: "upgrade rn for sample app" | Re-trigger Greptile

Comment thread javascript/mixpanel-network.js
Comment thread index.js

@tylerjroach tylerjroach left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few quick comments while I'm still doing a more thorough review.

Comment thread FEATURE_FLAGS_JS_MODE_FINDINGS.md Outdated
Comment thread FEATURE_FLAGS_QUICKSTART.md Outdated
Comment thread README.md Outdated
Comment thread ios/MixpanelReactNative.swift Outdated
efahk added 2 commits July 8, 2026 19:16
The module-scoped `jest` binding broke babel-plugin-jest-hoist's
out-of-scope rule for factories in jest.mock() calls after the
babel-preset upgrade merged from master.
@efahk efahk changed the title Adds feature flag support to main SDK Jul 8, 2026
Comment thread javascript/mixpanel-flags-js.js
Comment thread javascript/mixpanel-network.js
tylerjroach
tylerjroach previously approved these changes Jul 9, 2026
Comment thread javascript/mixpanel-flag-persistence.js
@tylerjroach tylerjroach changed the title feat: Adds feature flag support to main SDK Jul 10, 2026
@efahk
efahk merged commit 6c9c447 into master Jul 10, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants