Source:
@@ -289,7 +289,7 @@ Parameters:
Source:
@@ -377,7 +377,7 @@ clearChar
Source:
@@ -468,7 +468,7 @@ deleteUser<
Source:
@@ -630,7 +630,7 @@ Parameters:
Source:
@@ -792,7 +792,7 @@ Parameters:
Source:
@@ -954,7 +954,7 @@ Parameters:
Source:
@@ -1114,7 +1114,7 @@ Parameters:
Source:
@@ -1274,7 +1274,7 @@ Parameters:
Source:
@@ -1436,7 +1436,7 @@ Parameters:
Source:
@@ -1573,7 +1573,7 @@ Parameters:
Source:
@@ -1619,13 +1619,13 @@ Parameters:
diff --git a/docs/index.html b/docs/index.html
index 5df25796..80781900 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -94,31 +94,29 @@ Steps
2. Initialize Mixpanel
To start tracking with the library you must first initialize with your project token. You can get your project token from project settings.
-import { Mixpanel } from 'mixpanel-react-native';
+import { Mixpanel } from "mixpanel-react-native";
const trackAutomaticEvents = false;
const mixpanel = new Mixpanel("Your Project Token", trackAutomaticEvents);
mixpanel.init();
-
Once you've called this method once, you can access mixpanel throughout the rest of your application.
3. Send Data
Let's get started by sending event data. You can send an event from anywhere in your application. Better understand user behavior by storing details that are specific to the event (properties). After initializing the library, Mixpanel will automatically track some properties by default. learn more
// Track with event-name
-mixpanel.track('Sent Message');
+mixpanel.track("Sent Message");
// Track with event-name and property
-mixpanel.track('Plan Selected', {'Plan': 'Premium'});
+mixpanel.track("Plan Selected", { Plan: "Premium" });
In addition to event data, you can also send user profile data. We recommend this after completing the quickstart guide.
4. Check for Success
-Open up Events in Mixpanel to view incoming events.
+
Open up Events in Mixpanel to view incoming events.
Once data hits our API, it generally takes ~60 seconds for it to be processed, stored, and queryable in your project.
Complete Code Example
-
-import React from 'react';
+import React from "react";
import { Button, SafeAreaView } from "react-native";
-import { Mixpanel } from 'mixpanel-react-native';
+import { Mixpanel } from "mixpanel-react-native";
const trackAutomaticEvents = false;
const mixpanel = new Mixpanel("Your Project Token", trackAutomaticEvents);
@@ -129,15 +127,37 @@ Complete Code Example
<SafeAreaView>
<Button
title="Select Premium Plan"
- onPress={() => mixpanel.track("Plan Selected", {"Plan": "Premium"})}
+ onPress={() => mixpanel.track("Plan Selected", { Plan: "Premium" })}
/>
</SafeAreaView>
);
-}
+};
export default SampleApp;
-
+Feature Flags (Beta - 3.2.0-beta.1+)
+Control features dynamically and run A/B tests with Mixpanel Feature Flags. Native mode only (iOS/Android) in beta.
+Quick Start
+// Enable during initialization
+await mixpanel.init(false, {}, 'https://api.mixpanel.com', true, {
+ enabled: true,
+ context: { platform: 'mobile' } // Optional targeting context
+});
+
+// Check if feature is enabled
+if (mixpanel.flags.areFlagsReady()) {
+ const showNewUI = mixpanel.flags.isEnabledSync('new-feature', false);
+ const buttonColor = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+}
+
+Key Methods
+
+areFlagsReady() - Check if flags are loaded
+isEnabledSync(name, fallback) - Check if feature is enabled
+getVariantValueSync(name, fallback) - Get variant value
+getVariantSync(name, fallback) - Get full variant object with metadata
+
+All methods support async versions and snake_case aliases. See Feature Flags Quick Start Guide for complete documentation.
Expo and React Native for Web support (3.0.2 and above)
Starting from version 3.0.2, we have introduced support for Expo, React Native for Web, and other platforms utilizing React Native that do not support iOS and Android directly.
To enable this feature,
@@ -146,7 +166,7 @@
Expo and React Native for Web support (3.0.2 and above)
When JavaScript mode is enabled, Mixpanel utilizes AsyncStorage to persist data. If you prefer not to use it, or if AsyncStorage is unavailable in your target environment, you can import or define a different storage class. However, it must follow a subset (see: MixpanelAsyncStorage) of the same interface as AsyncStorage The following example demonstrates how to use a custom storage solution:
// Optional: if you do not want to use the default AsyncStorage
-const MyAsyncStorage = require("@my-org/<library-path>/AsyncStorage");
+const MyAsyncStorage = require("@my-org/<library-path>/AsyncStorage");
const trackAutomaticEvents = false;
const useNative = false;
const mixpanel = new Mixpanel('YOUR_TOKEN', trackAutomaticEvents, useNative, MyAsyncStorage);
@@ -163,29 +183,30 @@ Expo and React Native for Web support (3.0.2 and above)
);
This will activate JavaScript mode.
-👋 👋 Tell us about the Mixpanel developer experience! https://www.mixpanel.com/devnps 👍 👎
+👋 👋 Tell us about the Mixpanel developer experience! https://www.mixpanel.com/devnps 👍 👎
FAQ
-I want to stop tracking an event/event property in Mixpanel. Is that possible?
-Yes, in Lexicon, you can intercept and drop incoming events or properties. Mixpanel won’t store any new data for the event or property you select to drop. See this article for more information.
-I have a test user I would like to opt out of tracking. How do I do that?
-Mixpanel’s client-side tracking library contains the optOutTracking() method, which will set the user’s local opt-out state to “true” and will prevent data from being sent from a user’s device. More detailed instructions can be found in the section, Opting users out of tracking.
-Why aren't my events showing up?
-First, make sure your test device has internet access. To preserve battery life and customer bandwidth, the Mixpanel library doesn't send the events you record immediately. Instead, it sends batches to the Mixpanel servers every 60 seconds while your application is running, as well as when the application transitions to the background. You can call flush() manually if you want to force a flush at a particular moment.
+I want to stop tracking an event/event property in Mixpanel. Is that possible?
+Yes, in Lexicon, you can intercept and drop incoming events or properties. Mixpanel won’t store any new data for the event or property you select to drop. See this article for more information.
+I have a test user I would like to opt out of tracking. How do I do that?
+Mixpanel’s client-side tracking library contains the optOutTracking() method, which will set the user’s local opt-out state to “true” and will prevent data from being sent from a user’s device. More detailed instructions can be found in the section, Opting users out of tracking.
+Why aren't my events showing up?
+First, make sure your test device has internet access. To preserve battery life and customer bandwidth, the Mixpanel library doesn't send the events you record immediately. Instead, it sends batches to the Mixpanel servers every 60 seconds while your application is running, as well as when the application transitions to the background. You can call flush() manually if you want to force a flush at a particular moment.
mixpanel.flush();
-If your events are still not showing up after 60 seconds, check if you have opted out of tracking. You can also enable Mixpanel debugging and logging, it allows you to see the debug output from the Mixpanel library. To enable it, call setLoggingEnabled with true, then run your iOS project with Xcode or android project with Android Studio. The logs should be available in the console.
+If your events are still not showing up after 60 seconds, check if you have opted out of tracking. You can also enable Mixpanel debugging and logging, it allows you to see the debug output from the Mixpanel library. To enable it, call setLoggingEnabled with true, then run your iOS project with Xcode or android project with Android Studio. The logs should be available in the console.
mixpanel.setLoggingEnabled(true);
-Starting with iOS 14.5, do I need to request the user’s permission through the AppTrackingTransparency framework to use Mixpanel?
+
Starting with iOS 14.5, do I need to request the user’s permission through the AppTrackingTransparency framework to use Mixpanel?
No, Mixpanel does not use IDFA so it does not require user permission through the AppTrackingTransparency(ATT) framework.
-If I use Mixpanel, how do I answer app privacy questions for the App Store?
-Please refer to our Apple App Developer Privacy Guidance
+If I use Mixpanel, how do I answer app privacy questions for the App Store?
+Please refer to our Apple App Developer Privacy Guidance
I want to know more!
No worries, here are some links that you will find useful:
+
Have any questions? Reach out to Mixpanel Support to speak to someone smart, quickly.
@@ -197,13 +218,13 @@ I want to know more!
diff --git a/docs/index.js.html b/docs/index.js.html
index 4b542757..ccd8237b 100644
--- a/docs/index.js.html
+++ b/docs/index.js.html
@@ -74,6 +74,8 @@ Source: index.js
}
this.token = token;
this.trackAutomaticEvents = trackAutomaticEvents;
+ this._flags = null; // Lazy-loaded flags instance
+ this.storage = storage; // Store for JavaScript mode
if (useNative && MixpanelReactNative) {
this.mixpanelImpl = MixpanelReactNative;
@@ -88,25 +90,116 @@ Source: index.js
}
/**
- * Initializes Mixpanel
+ * Returns the Flags instance for feature flags operations.
*
- * @param {boolean} optOutTrackingDefault Optional Whether or not Mixpanel can start tracking by default. See optOutTracking()
- * @param {object} superProperties Optional A Map containing the key value pairs of the super properties to register
- * @param {string} serverURL Optional Set the base URL used for Mixpanel API requests. See setServerURL()
+ * <p>Feature Flags enable dynamic feature control and A/B testing capabilities.
+ * This property is lazy-loaded to avoid unnecessary initialization until first access.
*
+ * <p><b>Native Mode Only:</b> Feature flags are currently only available when using native mode
+ * (iOS/Android). JavaScript mode (Expo/React Native Web) support is planned for a future release.
+ *
+ * @return {Flags} an instance of Flags that provides access to feature flag operations
+ * @throws {Error} if accessed in JavaScript mode (when native modules are not available)
+ *
+ * @example
+ * // Check if flags are ready
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-checkout', false);
+ * }
+ *
+ * @example
+ * // Get a feature variant value
+ * const buttonColor = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ *
+ * @see Flags
+ */
+ get flags() {
+ // Short circuit for JavaScript mode - flags not ready for public use
+ if (this.mixpanelImpl !== MixpanelReactNative) {
+ throw new Error(
+ "Feature flags are only available in native mode. " +
+ "JavaScript mode support is coming in a future release."
+ );
+ }
+
+ if (!this._flags) {
+ // Lazy load the Flags instance with proper dependencies
+ const Flags = require("./javascript/mixpanel-flags").Flags;
+ this._flags = new Flags(this.token, this.mixpanelImpl, this.storage);
+ }
+ return this._flags;
+ }
+
+ /**
+ * Initializes Mixpanel with optional configuration for tracking, super properties, and feature flags.
+ *
+ * <p>This method must be called before using any other Mixpanel functionality. It sets up
+ * the tracking environment, registers super properties, and optionally initializes feature flags.
+ *
+ * @param {boolean} [optOutTrackingDefault=false] Whether or not Mixpanel can start tracking by default.
+ * If true, no data will be tracked until optInTracking() is called. See optOutTracking()
+ * @param {object} [superProperties={}] A Map containing the key value pairs of the super properties to register.
+ * These properties will be sent with every event. Pass {} if no super properties needed.
+ * @param {string} [serverURL="https://api.mixpanel.com"] The base URL used for Mixpanel API requests.
+ * Use "https://api-eu.mixpanel.com" for EU data residency. See setServerURL()
+ * @param {boolean} [useGzipCompression=false] Whether to use gzip compression for network requests.
+ * Enabling this reduces bandwidth usage but adds slight CPU overhead.
+ * @param {object} [featureFlagsOptions={}] Feature flags configuration object with the following properties:
+ * @param {boolean} [featureFlagsOptions.enabled=false] Whether to enable feature flags functionality
+ * @param {object} [featureFlagsOptions.context={}] Context properties used for feature flag targeting.
+ * Can include user properties, device properties, or any custom properties for flag evaluation.
+ * Note: In native mode, context must be set during initialization and cannot be updated later.
+ * @returns {Promise<void>} A promise that resolves when initialization is complete
+ *
+ * @example
+ * // Basic initialization
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init();
+ *
+ * @example
+ * // Initialize with feature flags enabled
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, {
+ * enabled: true,
+ * context: {
+ * platform: 'mobile',
+ * app_version: '2.1.0'
+ * }
+ * });
+ *
+ * @example
+ * // Initialize with EU data residency and super properties
+ * await mixpanel.init(
+ * false,
+ * { plan: 'premium', region: 'eu' },
+ * 'https://api-eu.mixpanel.com',
+ * true
+ * );
*/
async init(
optOutTrackingDefault = DEFAULT_OPT_OUT,
superProperties = {},
- serverURL = "https://api.mixpanel.com"
+ serverURL = "https://api.mixpanel.com",
+ useGzipCompression = false,
+ featureFlagsOptions = {}
) {
+ // Store feature flags options for later use
+ this.featureFlagsOptions = featureFlagsOptions;
+
await this.mixpanelImpl.initialize(
this.token,
this.trackAutomaticEvents,
optOutTrackingDefault,
{...Helper.getMetaData(), ...superProperties},
- serverURL
+ serverURL,
+ useGzipCompression,
+ featureFlagsOptions
);
+
+ // If flags are enabled AND we're in native mode, initialize them
+ if (featureFlagsOptions.enabled && this.mixpanelImpl === MixpanelReactNative) {
+ await this.flags.loadFlags();
+ }
}
/**
@@ -135,7 +228,9 @@ Source: index.js
trackAutomaticEvents,
optOutTrackingDefault,
Helper.getMetaData(),
- "https://api.mixpanel.com"
+ "https://api.mixpanel.com",
+ false,
+ {}
);
return new Mixpanel(token, trackAutomaticEvents);
}
@@ -1032,13 +1127,13 @@ Source: index.js
diff --git a/docs/javascript_mixpanel-flags.js.html b/docs/javascript_mixpanel-flags.js.html
new file mode 100644
index 00000000..0702367a
--- /dev/null
+++ b/docs/javascript_mixpanel-flags.js.html
@@ -0,0 +1,720 @@
+
+
+
+
+ JSDoc: Source: javascript/mixpanel-flags.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: javascript/mixpanel-flags.js
+
+
+
+
+
+
+
+
+ import { MixpanelFlagsJS } from './mixpanel-flags-js';
+
+/**
+ * Core class for using Mixpanel Feature Flags.
+ *
+ * <p>The Flags class provides access to Mixpanel's Feature Flags functionality, enabling
+ * dynamic feature control, A/B testing, and personalized user experiences. Feature flags
+ * allow you to remotely configure your app's features without deploying new code.
+ *
+ * <p>This class is accessed through the {@link Mixpanel#flags} property and is lazy-loaded
+ * to minimize performance impact until feature flags are actually used.
+ *
+ * <p><b>Platform Support:</b>
+ * <ul>
+ * <li><b>Native Mode (iOS/Android):</b> Fully supported with automatic experiment tracking</li>
+ * <li><b>JavaScript Mode (Expo/React Native Web):</b> Planned for future release</li>
+ * </ul>
+ *
+ * <p><b>Key Concepts:</b>
+ * <ul>
+ * <li><b>Feature Name:</b> The unique identifier for your feature flag (e.g., "new-checkout")</li>
+ * <li><b>Variant:</b> An object containing both a key and value representing the feature configuration</li>
+ * <li><b>Variant Key:</b> The identifier for the specific variation (e.g., "control", "treatment")</li>
+ * <li><b>Variant Value:</b> The actual configuration value (can be any JSON-serializable type)</li>
+ * <li><b>Fallback:</b> Default value returned when a flag is not available or not loaded</li>
+ * </ul>
+ *
+ * <p><b>Automatic Experiment Tracking:</b> When a feature flag is evaluated for the first time,
+ * Mixpanel automatically tracks a "$experiment_started" event with relevant metadata.
+ *
+ * @example
+ * // Initialize with feature flags enabled
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, {
+ * enabled: true,
+ * context: { platform: 'mobile' }
+ * });
+ *
+ * @example
+ * // Synchronous access (when flags are ready)
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-feature', false);
+ * const color = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ * const variant = mixpanel.flags.getVariantSync('checkout-flow', {
+ * key: 'control',
+ * value: 'standard'
+ * });
+ * }
+ *
+ * @example
+ * // Asynchronous access with Promise pattern
+ * const variant = await mixpanel.flags.getVariant('pricing-test', {
+ * key: 'control',
+ * value: { price: 9.99, currency: 'USD' }
+ * });
+ *
+ * @example
+ * // Asynchronous access with callback pattern
+ * mixpanel.flags.isEnabled('beta-features', false, (isEnabled) => {
+ * if (isEnabled) {
+ * // Enable beta features
+ * }
+ * });
+ *
+ * @see Mixpanel#flags
+ */
+export class Flags {
+ constructor(token, mixpanelImpl, storage) {
+ this.token = token;
+ this.mixpanelImpl = mixpanelImpl;
+ this.storage = storage;
+ this.isNativeMode = typeof mixpanelImpl.loadFlags === 'function';
+
+ // For JavaScript mode, create the JS implementation
+ if (!this.isNativeMode && storage) {
+ this.jsFlags = new MixpanelFlagsJS(token, mixpanelImpl, storage);
+ }
+ }
+
+ /**
+ * Manually fetch feature flags from the Mixpanel servers.
+ *
+ * <p>Feature flags are automatically loaded during initialization when feature flags are enabled.
+ * This method allows you to manually trigger a refresh of the flags, which is useful when:
+ * <ul>
+ * <li>You want to reload flags after a user property change</li>
+ * <li>You need to ensure you have the latest flag configuration</li>
+ * <li>Initial automatic load failed and you want to retry</li>
+ * </ul>
+ *
+ * <p>After successfully loading flags, {@link areFlagsReady} will return true and synchronous
+ * methods can be used to access flag values.
+ *
+ * @returns {Promise<void>} A promise that resolves when flags have been fetched and loaded
+ * @throws {Error} if feature flags are not initialized
+ *
+ * @example
+ * // Manually reload flags after user identification
+ * await mixpanel.identify('user123');
+ * await mixpanel.flags.loadFlags();
+ */
+ async loadFlags() {
+ if (this.isNativeMode) {
+ return await this.mixpanelImpl.loadFlags(this.token);
+ } else if (this.jsFlags) {
+ return await this.jsFlags.loadFlags();
+ }
+ throw new Error("Feature flags are not initialized");
+ }
+
+ /**
+ * Check if feature flags have been fetched from the server and are ready to use.
+ *
+ * <p>This method returns true after feature flags have been successfully loaded via {@link loadFlags}
+ * or during initialization. When flags are ready, you can safely use the synchronous methods
+ * ({@link getVariantSync}, {@link getVariantValueSync}, {@link isEnabledSync}) without waiting.
+ *
+ * <p>It's recommended to check this before using synchronous methods to ensure you're not
+ * getting fallback values due to flags not being loaded yet.
+ *
+ * @returns {boolean} true if flags have been loaded and are ready to use, false otherwise
+ *
+ * @example
+ * // Check before using synchronous methods
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-feature', false);
+ * } else {
+ * console.log('Flags not ready yet, using fallback');
+ * }
+ *
+ * @example
+ * // Wait for flags to be ready
+ * await mixpanel.flags.loadFlags();
+ * if (mixpanel.flags.areFlagsReady()) {
+ * // Now safe to use sync methods
+ * }
+ */
+ areFlagsReady() {
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.areFlagsReadySync(this.token);
+ } else if (this.jsFlags) {
+ return this.jsFlags.areFlagsReady();
+ }
+ return false;
+ }
+
+ /**
+ * Get a feature flag variant synchronously.
+ *
+ * <p>Returns the complete variant object for a feature flag, including both the variant key
+ * (e.g., "control", "treatment") and the variant value (the actual configuration data).
+ *
+ * <p><b>Important:</b> This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link getVariant} method instead.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {object} fallback The fallback variant object to return if the flag is not available.
+ * Must include both 'key' and 'value' properties.
+ * @returns {object} The flag variant object with the following structure:
+ * - key: {string} The variant key (e.g., "control", "treatment")
+ * - value: {any} The variant value (can be any JSON-serializable type)
+ * - experiment_id: {string|number} (optional) The experiment ID if this is an experiment
+ * - is_experiment_active: {boolean} (optional) Whether the experiment is currently active
+ *
+ * @example
+ * // Get a checkout flow variant
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const variant = mixpanel.flags.getVariantSync('checkout-flow', {
+ * key: 'control',
+ * value: 'standard'
+ * });
+ * console.log(`Using variant: ${variant.key}`);
+ * console.log(`Configuration: ${JSON.stringify(variant.value)}`);
+ * }
+ *
+ * @example
+ * // Get a complex configuration variant
+ * const defaultConfig = {
+ * key: 'default',
+ * value: {
+ * theme: 'light',
+ * layout: 'grid',
+ * itemsPerPage: 20
+ * }
+ * };
+ * const config = mixpanel.flags.getVariantSync('ui-config', defaultConfig);
+ *
+ * @see getVariant for asynchronous access
+ * @see getVariantValueSync to get only the value (not the full variant object)
+ */
+ getVariantSync(featureName, fallback) {
+ if (!this.areFlagsReady()) {
+ return fallback;
+ }
+
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.getVariantSync(this.token, featureName, fallback);
+ } else if (this.jsFlags) {
+ return this.jsFlags.getVariantSync(featureName, fallback);
+ }
+ return fallback;
+ }
+
+ /**
+ * Get a feature flag variant value synchronously.
+ *
+ * <p>Returns only the value portion of a feature flag variant, without the variant key or metadata.
+ * This is useful when you only care about the configuration data, not which variant was selected.
+ *
+ * <p><b>Important:</b> This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link getVariantValue} method instead.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {any} fallbackValue The fallback value to return if the flag is not available.
+ * Can be any JSON-serializable type (string, number, boolean, object, array, etc.)
+ * @returns {any} The flag's value, or the fallback if the flag is not available.
+ * The return type matches the type of value configured in your Mixpanel project.
+ *
+ * @example
+ * // Get a simple string value
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const buttonColor = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ * applyButtonColor(buttonColor);
+ * }
+ *
+ * @example
+ * // Get a complex object value
+ * const defaultPricing = { price: 9.99, currency: 'USD', trial_days: 7 };
+ * const pricing = mixpanel.flags.getVariantValueSync('pricing-config', defaultPricing);
+ * console.log(`Price: ${pricing.price} ${pricing.currency}`);
+ *
+ * @example
+ * // Get a boolean value
+ * const showPromo = mixpanel.flags.getVariantValueSync('show-promo', false);
+ * if (showPromo) {
+ * displayPromotionalBanner();
+ * }
+ *
+ * @see getVariantValue for asynchronous access
+ * @see getVariantSync to get the full variant object including key and metadata
+ */
+ getVariantValueSync(featureName, fallbackValue) {
+ if (!this.areFlagsReady()) {
+ return fallbackValue;
+ }
+
+ if (this.isNativeMode) {
+ // Android returns a wrapped object due to React Native limitations
+ const result = this.mixpanelImpl.getVariantValueSync(this.token, featureName, fallbackValue);
+ if (result && typeof result === 'object' && 'type' in result) {
+ // Android wraps the response
+ return result.type === 'fallback' ? fallbackValue : result.value;
+ }
+ // iOS returns the value directly
+ return result;
+ } else if (this.jsFlags) {
+ return this.jsFlags.getVariantValueSync(featureName, fallbackValue);
+ }
+ return fallbackValue;
+ }
+
+ /**
+ * Check if a feature flag is enabled synchronously.
+ *
+ * <p>This is a convenience method for boolean feature flags. It checks if a feature is enabled
+ * by evaluating the variant value as a boolean. A feature is considered "enabled" when its
+ * variant value evaluates to true.
+ *
+ * <p><b>Important:</b> This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link isEnabled} method instead.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {boolean} [fallbackValue=false] The fallback value to return if the flag is not available.
+ * Defaults to false if not provided.
+ * @returns {boolean} true if the feature is enabled, false otherwise
+ *
+ * @example
+ * // Simple feature toggle
+ * if (mixpanel.flags.areFlagsReady()) {
+ * if (mixpanel.flags.isEnabledSync('new-checkout', false)) {
+ * showNewCheckout();
+ * } else {
+ * showLegacyCheckout();
+ * }
+ * }
+ *
+ * @example
+ * // With explicit fallback
+ * const enableBetaFeatures = mixpanel.flags.isEnabledSync('beta-features', true);
+ *
+ * @see isEnabled for asynchronous access
+ * @see getVariantValueSync for non-boolean flag values
+ */
+ isEnabledSync(featureName, fallbackValue = false) {
+ if (!this.areFlagsReady()) {
+ return fallbackValue;
+ }
+
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.isEnabledSync(this.token, featureName, fallbackValue);
+ } else if (this.jsFlags) {
+ return this.jsFlags.isEnabledSync(featureName, fallbackValue);
+ }
+ return fallbackValue;
+ }
+
+ /**
+ * Get a feature flag variant asynchronously.
+ *
+ * <p>Returns the complete variant object for a feature flag, including both the variant key
+ * and the variant value. This method works regardless of whether flags are ready, making it
+ * safe to use at any time.
+ *
+ * <p>Supports both Promise and callback patterns for maximum flexibility.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {object} fallback The fallback variant object to return if the flag is not available.
+ * Must include both 'key' and 'value' properties.
+ * @param {function} [callback] Optional callback function that receives the variant object.
+ * If provided, the method returns void. If omitted, the method returns a Promise.
+ * @returns {Promise<object>|void} Promise that resolves to the variant object if no callback provided,
+ * void if callback is provided. The variant object has the following structure:
+ * - key: {string} The variant key (e.g., "control", "treatment")
+ * - value: {any} The variant value (can be any JSON-serializable type)
+ * - experiment_id: {string|number} (optional) The experiment ID if this is an experiment
+ * - is_experiment_active: {boolean} (optional) Whether the experiment is currently active
+ *
+ * @example
+ * // Promise pattern (recommended)
+ * const variant = await mixpanel.flags.getVariant('checkout-flow', {
+ * key: 'control',
+ * value: 'standard'
+ * });
+ * console.log(`Using ${variant.key}: ${variant.value}`);
+ *
+ * @example
+ * // Callback pattern
+ * mixpanel.flags.getVariant('pricing-test', {
+ * key: 'default',
+ * value: { price: 9.99 }
+ * }, (variant) => {
+ * console.log(`Price: ${variant.value.price}`);
+ * });
+ *
+ * @see getVariantSync for synchronous access when flags are ready
+ * @see getVariantValue to get only the value without the variant key
+ */
+ getVariant(featureName, fallback, callback) {
+ // If callback provided, use callback pattern
+ if (typeof callback === 'function') {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.getVariant(this.token, featureName, fallback)
+ .then(result => callback(result))
+ .catch(() => callback(fallback));
+ } else if (this.jsFlags) {
+ this.jsFlags.getVariant(featureName, fallback)
+ .then(result => callback(result))
+ .catch(() => callback(fallback));
+ } else {
+ callback(fallback);
+ }
+ return;
+ }
+
+ // Promise pattern
+ return new Promise((resolve) => {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.getVariant(this.token, featureName, fallback)
+ .then(resolve)
+ .catch(() => resolve(fallback));
+ } else if (this.jsFlags) {
+ this.jsFlags.getVariant(featureName, fallback)
+ .then(resolve)
+ .catch(() => resolve(fallback));
+ } else {
+ resolve(fallback);
+ }
+ });
+ }
+
+ /**
+ * Get a feature flag variant value asynchronously.
+ *
+ * <p>Returns only the value portion of a feature flag variant. This method works regardless
+ * of whether flags are ready, making it safe to use at any time.
+ *
+ * <p>Supports both Promise and callback patterns for maximum flexibility.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {any} fallbackValue The fallback value to return if the flag is not available.
+ * Can be any JSON-serializable type (string, number, boolean, object, array, etc.)
+ * @param {function} [callback] Optional callback function that receives the flag value.
+ * If provided, the method returns void. If omitted, the method returns a Promise.
+ * @returns {Promise<any>|void} Promise that resolves to the flag value if no callback provided,
+ * void if callback is provided. The return type matches the type of value configured in
+ * your Mixpanel project.
+ *
+ * @example
+ * // Promise pattern (recommended)
+ * const buttonColor = await mixpanel.flags.getVariantValue('button-color', 'blue');
+ * applyButtonColor(buttonColor);
+ *
+ * @example
+ * // Promise pattern with object value
+ * const pricing = await mixpanel.flags.getVariantValue('pricing-config', {
+ * price: 9.99,
+ * currency: 'USD'
+ * });
+ * displayPrice(pricing.price, pricing.currency);
+ *
+ * @example
+ * // Callback pattern
+ * mixpanel.flags.getVariantValue('theme', 'light', (theme) => {
+ * applyTheme(theme);
+ * });
+ *
+ * @see getVariantValueSync for synchronous access when flags are ready
+ * @see getVariant to get the full variant object including key and metadata
+ */
+ getVariantValue(featureName, fallbackValue, callback) {
+ // If callback provided, use callback pattern
+ if (typeof callback === 'function') {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.getVariantValue(this.token, featureName, fallbackValue)
+ .then(result => callback(result))
+ .catch(() => callback(fallbackValue));
+ } else if (this.jsFlags) {
+ this.jsFlags.getVariantValue(featureName, fallbackValue)
+ .then(result => callback(result))
+ .catch(() => callback(fallbackValue));
+ } else {
+ callback(fallbackValue);
+ }
+ return;
+ }
+
+ // Promise pattern
+ return new Promise((resolve) => {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.getVariantValue(this.token, featureName, fallbackValue)
+ .then(resolve)
+ .catch(() => resolve(fallbackValue));
+ } else if (this.jsFlags) {
+ this.jsFlags.getVariantValue(featureName, fallbackValue)
+ .then(resolve)
+ .catch(() => resolve(fallbackValue));
+ } else {
+ resolve(fallbackValue);
+ }
+ });
+ }
+
+ /**
+ * Check if a feature flag is enabled asynchronously.
+ *
+ * <p>This is a convenience method for boolean feature flags. It checks if a feature is enabled
+ * by evaluating the variant value as a boolean. This method works regardless of whether flags
+ * are ready, making it safe to use at any time.
+ *
+ * <p>Supports both Promise and callback patterns for maximum flexibility.
+ *
+ * <p>When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {boolean} [fallbackValue=false] The fallback value to return if the flag is not available.
+ * Defaults to false if not provided.
+ * @param {function} [callback] Optional callback function that receives the boolean result.
+ * If provided, the method returns void. If omitted, the method returns a Promise.
+ * @returns {Promise<boolean>|void} Promise that resolves to true if enabled, false otherwise
+ * (when no callback provided). Returns void if callback is provided.
+ *
+ * @example
+ * // Promise pattern (recommended)
+ * const isEnabled = await mixpanel.flags.isEnabled('new-checkout', false);
+ * if (isEnabled) {
+ * showNewCheckout();
+ * } else {
+ * showLegacyCheckout();
+ * }
+ *
+ * @example
+ * // Callback pattern
+ * mixpanel.flags.isEnabled('beta-features', false, (isEnabled) => {
+ * if (isEnabled) {
+ * enableBetaFeatures();
+ * }
+ * });
+ *
+ * @example
+ * // Default fallback (false)
+ * const showPromo = await mixpanel.flags.isEnabled('show-promo');
+ *
+ * @see isEnabledSync for synchronous access when flags are ready
+ * @see getVariantValue for non-boolean flag values
+ */
+ isEnabled(featureName, fallbackValue = false, callback) {
+ // If callback provided, use callback pattern
+ if (typeof callback === 'function') {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.isEnabled(this.token, featureName, fallbackValue)
+ .then(result => callback(result))
+ .catch(() => callback(fallbackValue));
+ } else if (this.jsFlags) {
+ this.jsFlags.isEnabled(featureName, fallbackValue)
+ .then(result => callback(result))
+ .catch(() => callback(fallbackValue));
+ } else {
+ callback(fallbackValue);
+ }
+ return;
+ }
+
+ // Promise pattern
+ return new Promise((resolve) => {
+ if (this.isNativeMode) {
+ this.mixpanelImpl.isEnabled(this.token, featureName, fallbackValue)
+ .then(resolve)
+ .catch(() => resolve(fallbackValue));
+ } else if (this.jsFlags) {
+ this.jsFlags.isEnabled(featureName, fallbackValue)
+ .then(resolve)
+ .catch(() => resolve(fallbackValue));
+ } else {
+ resolve(fallbackValue);
+ }
+ });
+ }
+
+ /**
+ * Update the context used for feature flag evaluation.
+ *
+ * <p>Context properties are used to determine which feature flag variants a user should receive
+ * based on targeting rules configured in your Mixpanel project. This allows for personalized
+ * feature experiences based on user attributes, device properties, or custom criteria.
+ *
+ * <p><b>IMPORTANT LIMITATION:</b> This method is <b>only available in JavaScript mode</b>
+ * (Expo/React Native Web). In native mode (iOS/Android), context must be set during initialization
+ * via {@link Mixpanel#init} and cannot be updated at runtime.
+ *
+ * <p>By default, the new context properties are merged with existing context. Set
+ * <code>options.replace = true</code> to completely replace the context instead.
+ *
+ * @param {object} newContext New context properties to add or update. Can include any
+ * JSON-serializable properties that are used in your feature flag targeting rules.
+ * Common examples include user tier, region, platform version, etc.
+ * @param {object} [options={replace: false}] Configuration options for the update
+ * @param {boolean} [options.replace=false] If true, replaces the entire context instead of merging.
+ * If false (default), merges new properties with existing context.
+ * @returns {Promise<void>} A promise that resolves when the context has been updated and
+ * flags have been re-evaluated with the new context
+ * @throws {Error} if called in native mode (iOS/Android)
+ *
+ * @example
+ * // Merge new properties into existing context (JavaScript mode only)
+ * await mixpanel.flags.updateContext({
+ * user_tier: 'premium',
+ * region: 'us-west'
+ * });
+ *
+ * @example
+ * // Replace entire context (JavaScript mode only)
+ * await mixpanel.flags.updateContext({
+ * device_type: 'tablet',
+ * os_version: '14.0'
+ * }, { replace: true });
+ *
+ * @example
+ * // This will throw an error in native mode
+ * try {
+ * await mixpanel.flags.updateContext({ tier: 'premium' });
+ * } catch (error) {
+ * console.error('Context updates not supported in native mode');
+ * }
+ */
+ async updateContext(newContext, options = { replace: false }) {
+ if (this.isNativeMode) {
+ throw new Error(
+ "updateContext() is not supported in native mode. " +
+ "Context must be set during initialization via FeatureFlagsOptions. " +
+ "This feature is only available in JavaScript mode (Expo/React Native Web)."
+ );
+ } else if (this.jsFlags) {
+ return await this.jsFlags.updateContext(newContext, options);
+ }
+ throw new Error("Feature flags are not initialized");
+ }
+
+ // snake_case aliases for API consistency with mixpanel-js
+
+ /**
+ * Alias for {@link areFlagsReady}. Provided for API consistency with mixpanel-js.
+ * @see areFlagsReady
+ */
+ are_flags_ready() {
+ return this.areFlagsReady();
+ }
+
+ /**
+ * Alias for {@link getVariant}. Provided for API consistency with mixpanel-js.
+ * @see getVariant
+ */
+ get_variant(featureName, fallback, callback) {
+ return this.getVariant(featureName, fallback, callback);
+ }
+
+ /**
+ * Alias for {@link getVariantSync}. Provided for API consistency with mixpanel-js.
+ * @see getVariantSync
+ */
+ get_variant_sync(featureName, fallback) {
+ return this.getVariantSync(featureName, fallback);
+ }
+
+ /**
+ * Alias for {@link getVariantValue}. Provided for API consistency with mixpanel-js.
+ * @see getVariantValue
+ */
+ get_variant_value(featureName, fallbackValue, callback) {
+ return this.getVariantValue(featureName, fallbackValue, callback);
+ }
+
+ /**
+ * Alias for {@link getVariantValueSync}. Provided for API consistency with mixpanel-js.
+ * @see getVariantValueSync
+ */
+ get_variant_value_sync(featureName, fallbackValue) {
+ return this.getVariantValueSync(featureName, fallbackValue);
+ }
+
+ /**
+ * Alias for {@link isEnabled}. Provided for API consistency with mixpanel-js.
+ * @see isEnabled
+ */
+ is_enabled(featureName, fallbackValue, callback) {
+ return this.isEnabled(featureName, fallbackValue, callback);
+ }
+
+ /**
+ * Alias for {@link isEnabledSync}. Provided for API consistency with mixpanel-js.
+ * @see isEnabledSync
+ */
+ is_enabled_sync(featureName, fallbackValue) {
+ return this.isEnabledSync(featureName, fallbackValue);
+ }
+
+ /**
+ * Alias for {@link updateContext}. Provided for API consistency with mixpanel-js.
+ * JavaScript mode only.
+ * @see updateContext
+ */
+ update_context(newContext, options) {
+ return this.updateContext(newContext, options);
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.d.ts b/index.d.ts
index 600ab091..c77f85d8 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -7,7 +7,69 @@ export type MixpanelAsyncStorage = {
removeItem(key: string): Promise;
};
+export interface MixpanelFlagVariant {
+ key: string;
+ value: any;
+ experiment_id?: string | number; // Updated to match mixpanel-js format
+ is_experiment_active?: boolean; // Updated to match mixpanel-js format
+ is_qa_tester?: boolean; // Updated to match mixpanel-js format
+}
+
+export interface FeatureFlagsOptions {
+ enabled?: boolean;
+ context?: {
+ [key: string]: any;
+ custom_properties?: {
+ [key: string]: any;
+ };
+ };
+}
+
+export interface UpdateContextOptions {
+ replace?: boolean;
+}
+
+export interface Flags {
+ // Synchronous methods
+ loadFlags(): Promise;
+ areFlagsReady(): boolean;
+ getVariantSync(featureName: string, fallback: MixpanelFlagVariant): MixpanelFlagVariant;
+ getVariantValueSync(featureName: string, fallbackValue: any): any;
+ isEnabledSync(featureName: string, fallbackValue?: boolean): boolean;
+
+ // Asynchronous methods with overloads for callback and Promise patterns
+ getVariant(featureName: string, fallback: MixpanelFlagVariant): Promise;
+ getVariant(featureName: string, fallback: MixpanelFlagVariant, callback: (result: MixpanelFlagVariant) => void): void;
+
+ getVariantValue(featureName: string, fallbackValue: any): Promise;
+ getVariantValue(featureName: string, fallbackValue: any, callback: (value: any) => void): void;
+
+ isEnabled(featureName: string, fallbackValue?: boolean): Promise;
+ isEnabled(featureName: string, fallbackValue: boolean, callback: (isEnabled: boolean) => void): void;
+
+ // Context management (NEW - aligned with mixpanel-js)
+ // NOTE: Only available in JavaScript mode (Expo/React Native Web)
+ // In native mode, throws an error - context must be set during initialization
+ updateContext(newContext: MixpanelProperties, options?: UpdateContextOptions): Promise;
+
+ // snake_case aliases (NEW - aligned with mixpanel-js)
+ are_flags_ready(): boolean;
+ get_variant(featureName: string, fallback: MixpanelFlagVariant): Promise;
+ get_variant(featureName: string, fallback: MixpanelFlagVariant, callback: (result: MixpanelFlagVariant) => void): void;
+ get_variant_sync(featureName: string, fallback: MixpanelFlagVariant): MixpanelFlagVariant;
+ get_variant_value(featureName: string, fallbackValue: any): Promise;
+ get_variant_value(featureName: string, fallbackValue: any, callback: (value: any) => void): void;
+ get_variant_value_sync(featureName: string, fallbackValue: any): any;
+ is_enabled(featureName: string, fallbackValue?: boolean): Promise;
+ is_enabled(featureName: string, fallbackValue: boolean, callback: (isEnabled: boolean) => void): void;
+ is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
+ // NOTE: Only available in JavaScript mode (Expo/React Native Web)
+ update_context(newContext: MixpanelProperties, options?: UpdateContextOptions): Promise;
+}
+
export class Mixpanel {
+ readonly flags: Flags;
+
constructor(token: string, trackAutoMaticEvents: boolean);
constructor(token: string, trackAutoMaticEvents: boolean, useNative: true);
constructor(
@@ -25,7 +87,8 @@ export class Mixpanel {
optOutTrackingDefault?: boolean,
superProperties?: MixpanelProperties,
serverURL?: string,
- useGzipCompression?: boolean
+ useGzipCompression?: boolean,
+ featureFlagsOptions?: FeatureFlagsOptions
): Promise;
setServerURL(serverURL: string): void;
setLoggingEnabled(loggingEnabled: boolean): void;
diff --git a/index.js b/index.js
index 943e248a..f6019876 100644
--- a/index.js
+++ b/index.js
@@ -46,6 +46,8 @@ export class Mixpanel {
}
this.token = token;
this.trackAutomaticEvents = trackAutomaticEvents;
+ this._flags = null; // Lazy-loaded flags instance
+ this.storage = storage; // Store for JavaScript mode
if (useNative && MixpanelReactNative) {
this.mixpanelImpl = MixpanelReactNative;
@@ -60,27 +62,116 @@ export class Mixpanel {
}
/**
- * Initializes Mixpanel
+ * Returns the Flags instance for feature flags operations.
*
- * @param {boolean} optOutTrackingDefault Optional Whether or not Mixpanel can start tracking by default. See optOutTracking()
- * @param {object} superProperties Optional A Map containing the key value pairs of the super properties to register
- * @param {string} serverURL Optional Set the base URL used for Mixpanel API requests. See setServerURL()
- * @param {boolean} useGzipCompression Optional Set whether to use gzip compression for network requests. Defaults to false.
+ * Feature Flags enable dynamic feature control and A/B testing capabilities.
+ * This property is lazy-loaded to avoid unnecessary initialization until first access.
+ *
+ *
Native Mode Only: Feature flags are currently only available when using native mode
+ * (iOS/Android). JavaScript mode (Expo/React Native Web) support is planned for a future release.
+ *
+ * @return {Flags} an instance of Flags that provides access to feature flag operations
+ * @throws {Error} if accessed in JavaScript mode (when native modules are not available)
+ *
+ * @example
+ * // Check if flags are ready
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-checkout', false);
+ * }
+ *
+ * @example
+ * // Get a feature variant value
+ * const buttonColor = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ *
+ * @see Flags
+ */
+ get flags() {
+ // Short circuit for JavaScript mode - flags not ready for public use
+ if (this.mixpanelImpl !== MixpanelReactNative) {
+ throw new Error(
+ "Feature flags are only available in native mode. " +
+ "JavaScript mode support is coming in a future release."
+ );
+ }
+
+ if (!this._flags) {
+ // Lazy load the Flags instance with proper dependencies
+ const Flags = require("./javascript/mixpanel-flags").Flags;
+ this._flags = new Flags(this.token, this.mixpanelImpl, this.storage);
+ }
+ return this._flags;
+ }
+
+ /**
+ * Initializes Mixpanel with optional configuration for tracking, super properties, and feature flags.
+ *
+ *
This method must be called before using any other Mixpanel functionality. It sets up
+ * the tracking environment, registers super properties, and optionally initializes feature flags.
+ *
+ * @param {boolean} [optOutTrackingDefault=false] Whether or not Mixpanel can start tracking by default.
+ * If true, no data will be tracked until optInTracking() is called. See optOutTracking()
+ * @param {object} [superProperties={}] A Map containing the key value pairs of the super properties to register.
+ * These properties will be sent with every event. Pass {} if no super properties needed.
+ * @param {string} [serverURL="https://api.mixpanel.com"] The base URL used for Mixpanel API requests.
+ * Use "https://api-eu.mixpanel.com" for EU data residency. See setServerURL()
+ * @param {boolean} [useGzipCompression=false] Whether to use gzip compression for network requests.
+ * Enabling this reduces bandwidth usage but adds slight CPU overhead.
+ * @param {object} [featureFlagsOptions={}] Feature flags configuration object with the following properties:
+ * @param {boolean} [featureFlagsOptions.enabled=false] Whether to enable feature flags functionality
+ * @param {object} [featureFlagsOptions.context={}] Context properties used for feature flag targeting.
+ * Can include user properties, device properties, or any custom properties for flag evaluation.
+ * Note: In native mode, context must be set during initialization and cannot be updated later.
+ * @returns {Promise} A promise that resolves when initialization is complete
+ *
+ * @example
+ * // Basic initialization
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init();
+ *
+ * @example
+ * // Initialize with feature flags enabled
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, {
+ * enabled: true,
+ * context: {
+ * platform: 'mobile',
+ * app_version: '2.1.0'
+ * }
+ * });
+ *
+ * @example
+ * // Initialize with EU data residency and super properties
+ * await mixpanel.init(
+ * false,
+ * { plan: 'premium', region: 'eu' },
+ * 'https://api-eu.mixpanel.com',
+ * true
+ * );
*/
async init(
optOutTrackingDefault = DEFAULT_OPT_OUT,
superProperties = {},
serverURL = "https://api.mixpanel.com",
- useGzipCompression = false
+ useGzipCompression = false,
+ featureFlagsOptions = {}
) {
+ // Store feature flags options for later use
+ this.featureFlagsOptions = featureFlagsOptions;
+
await this.mixpanelImpl.initialize(
this.token,
this.trackAutomaticEvents,
optOutTrackingDefault,
{...Helper.getMetaData(), ...superProperties},
serverURL,
- useGzipCompression
+ useGzipCompression,
+ featureFlagsOptions
);
+
+ // If flags are enabled AND we're in native mode, initialize them
+ if (featureFlagsOptions.enabled && this.mixpanelImpl === MixpanelReactNative) {
+ await this.flags.loadFlags();
+ }
}
/**
@@ -109,7 +200,9 @@ export class Mixpanel {
trackAutomaticEvents,
optOutTrackingDefault,
Helper.getMetaData(),
- "https://api.mixpanel.com"
+ "https://api.mixpanel.com",
+ false,
+ {}
);
return new Mixpanel(token, trackAutomaticEvents);
}
diff --git a/ios/MixpanelReactNative.m b/ios/MixpanelReactNative.m
index bd29c1e5..ef63b1df 100644
--- a/ios/MixpanelReactNative.m
+++ b/ios/MixpanelReactNative.m
@@ -5,7 +5,7 @@ @interface RCT_EXTERN_MODULE(MixpanelReactNative, NSObject)
// MARK: - Mixpanel Instance
-RCT_EXTERN_METHOD(initialize:(NSString *)token trackAutomaticEvents:(BOOL)trackAutomaticEvents optOutTrackingByDefault:(BOOL)optOutTrackingByDefault properties:(NSDictionary *)properties serverURL:(NSString *)serverURL useGzipCompression:(BOOL)useGzipCompression resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+RCT_EXTERN_METHOD(initialize:(NSString *)token trackAutomaticEvents:(BOOL)trackAutomaticEvents optOutTrackingByDefault:(BOOL)optOutTrackingByDefault properties:(NSDictionary *)properties serverURL:(NSString *)serverURL useGzipCompression:(BOOL)useGzipCompression featureFlagsOptions:(NSDictionary *)featureFlagsOptions resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
// Mark: - Settings
RCT_EXTERN_METHOD(setServerURL:(NSString *)token serverURL:(NSString *)serverURL resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@@ -105,4 +105,22 @@ @interface RCT_EXTERN_MODULE(MixpanelReactNative, NSObject)
RCT_EXTERN_METHOD(groupUnionProperty:(NSString *)token groupKey:(NSString *)groupKey groupID:(id)groupID name:(NSString *)name values:(NSArray *)values resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+// MARK: - Feature Flags
+
+RCT_EXTERN_METHOD(loadFlags:(NSString *)token resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(areFlagsReadySync:(NSString *)token)
+
+RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getVariantSync:(NSString *)token featureName:(NSString *)featureName fallback:(NSDictionary *)fallback)
+
+RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getVariantValueSync:(NSString *)token featureName:(NSString *)featureName fallbackValue:(id)fallbackValue)
+
+RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(isEnabledSync:(NSString *)token featureName:(NSString *)featureName fallbackValue:(BOOL)fallbackValue)
+
+RCT_EXTERN_METHOD(getVariant:(NSString *)token featureName:(NSString *)featureName fallback:(NSDictionary *)fallback resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getVariantValue:(NSString *)token featureName:(NSString *)featureName fallbackValue:(id)fallbackValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(isEnabled:(NSString *)token featureName:(NSString *)featureName fallbackValue:(BOOL)fallbackValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
@end
diff --git a/ios/MixpanelReactNative.swift b/ios/MixpanelReactNative.swift
index 855185e5..45995c14 100644
--- a/ios/MixpanelReactNative.swift
+++ b/ios/MixpanelReactNative.swift
@@ -18,16 +18,39 @@ open class MixpanelReactNative: NSObject {
properties: [String: Any],
serverURL: String,
useGzipCompression: Bool = false,
+ featureFlagsOptions: [String: Any]?,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) -> Void {
let autoProps = properties // copy
AutomaticProperties.setAutomaticProperties(autoProps)
let propsProcessed = MixpanelTypeHandler.processProperties(properties: autoProps)
- Mixpanel.initialize(token: token, trackAutomaticEvents: trackAutomaticEvents, flushInterval: Constants.DEFAULT_FLUSH_INTERVAL,
- instanceName: token, optOutTrackingByDefault: optOutTrackingByDefault,
- superProperties: propsProcessed,
- serverURL: serverURL,
- useGzipCompression: useGzipCompression)
+
+ // Handle feature flags options
+ var featureFlagsEnabled = false
+ var featureFlagsContext: [String: Any]? = nil
+
+ if let flagsOptions = featureFlagsOptions {
+ featureFlagsEnabled = flagsOptions["enabled"] as? Bool ?? false
+ featureFlagsContext = flagsOptions["context"] as? [String: Any]
+ }
+
+ // Create MixpanelOptions with all configuration including feature flags
+ let options = MixpanelOptions(
+ token: token,
+ flushInterval: Constants.DEFAULT_FLUSH_INTERVAL,
+ instanceName: token,
+ trackAutomaticEvents: trackAutomaticEvents,
+ optOutTrackingByDefault: optOutTrackingByDefault,
+ useUniqueDistinctId: false,
+ superProperties: propsProcessed,
+ serverURL: serverURL,
+ proxyServerConfig: nil,
+ useGzipCompression: useGzipCompression,
+ featureFlagsEnabled: featureFlagsEnabled,
+ featureFlagsContext: featureFlagsContext ?? [:]
+ )
+
+ Mixpanel.initialize(options: options)
resolve(true)
}
@@ -460,4 +483,159 @@ open class MixpanelReactNative: NSObject {
return Mixpanel.getInstance(name: token)
}
+ // MARK: - Feature Flags
+
+ @objc
+ func loadFlags(_ token: String,
+ resolver resolve: RCTPromiseResolveBlock,
+ rejecter reject: RCTPromiseRejectBlock) -> Void {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ resolve(nil)
+ return
+ }
+ flags.loadFlags()
+ resolve(nil)
+ }
+
+ @objc
+ func areFlagsReadySync(_ token: String) -> NSNumber {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token) else {
+ NSLog("[Mixpanel - areFlagsReadySync: instance is nil for token: \(token)]")
+ return NSNumber(value: false)
+ }
+
+ guard let flags = instance.flags else {
+ NSLog("[Mixpanel - areFlagsReadySync: flags is nil")
+ return NSNumber(value: false)
+ }
+
+ let ready = flags.areFlagsReady()
+ NSLog("[Mixpanel - areFlagsReadySync: flags ready = \(ready)")
+ return NSNumber(value: ready)
+ }
+
+ @objc
+ func getVariantSync(_ token: String,
+ featureName: String,
+ fallback: [String: Any]) -> [String: Any] {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ return fallback
+ }
+
+ let fallbackVariant = convertDictToVariant(fallback)
+ let variant = flags.getVariantSync(featureName, fallback: fallbackVariant)
+ return convertVariantToDict(variant)
+ }
+
+ @objc
+ func getVariantValueSync(_ token: String,
+ featureName: String,
+ fallbackValue: Any) -> Any {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ return fallbackValue
+ }
+
+ return flags.getVariantValueSync(featureName, fallbackValue: fallbackValue) ?? fallbackValue
+ }
+
+ @objc
+ func isEnabledSync(_ token: String,
+ featureName: String,
+ fallbackValue: Bool) -> NSNumber {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ return NSNumber(value: fallbackValue)
+ }
+
+ let enabled = flags.isEnabledSync(featureName, fallbackValue: fallbackValue)
+ return NSNumber(value: enabled)
+ }
+
+ @objc
+ func getVariant(_ token: String,
+ featureName: String,
+ fallback: [String: Any],
+ resolver resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ resolve(fallback)
+ return
+ }
+
+ let fallbackVariant = convertDictToVariant(fallback)
+ flags.getVariant(featureName, fallback: fallbackVariant) { variant in
+ resolve(self.convertVariantToDict(variant))
+ }
+ }
+
+ @objc
+ func getVariantValue(_ token: String,
+ featureName: String,
+ fallbackValue: Any,
+ resolver resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ resolve(fallbackValue)
+ return
+ }
+
+ flags.getVariantValue(featureName, fallbackValue: fallbackValue) { value in
+ resolve(value)
+ }
+ }
+
+ @objc
+ func isEnabled(_ token: String,
+ featureName: String,
+ fallbackValue: Bool,
+ resolver resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
+ guard let instance = MixpanelReactNative.getMixpanelInstance(token),
+ let flags = instance.flags else {
+ resolve(fallbackValue)
+ return
+ }
+
+ flags.isEnabled(featureName, fallbackValue: fallbackValue) { isEnabled in
+ resolve(isEnabled)
+ }
+ }
+
+ // Helper methods for variant conversion
+ private func convertDictToVariant(_ dict: [String: Any]) -> MixpanelFlagVariant {
+ let key = dict["key"] as? String ?? ""
+ let value = dict["value"] ?? NSNull()
+ let experimentID = dict["experimentID"] as? String
+ let isExperimentActive = dict["isExperimentActive"] as? Bool
+ let isQATester = dict["isQATester"] as? Bool
+
+ return MixpanelFlagVariant(
+ key: key,
+ value: value,
+ isExperimentActive: isExperimentActive,
+ isQATester: isQATester,
+ experimentID: experimentID
+ )
+ }
+
+ private func convertVariantToDict(_ variant: MixpanelFlagVariant) -> [String: Any] {
+ var dict: [String: Any] = [
+ "key": variant.key,
+ "value": variant.value ?? NSNull()
+ ]
+
+ if let experimentID = variant.experimentID {
+ dict["experimentID"] = experimentID
+ }
+ dict["isExperimentActive"] = variant.isExperimentActive
+ dict["isQATester"] = variant.isQATester
+
+ return dict
+ }
+
}
diff --git a/javascript/mixpanel-flags-js.js b/javascript/mixpanel-flags-js.js
new file mode 100644
index 00000000..ec6a77c8
--- /dev/null
+++ b/javascript/mixpanel-flags-js.js
@@ -0,0 +1,463 @@
+import { MixpanelLogger } from "./mixpanel-logger";
+import { MixpanelNetwork } from "./mixpanel-network";
+import { MixpanelPersistent } from "./mixpanel-persistent";
+import packageJson from "mixpanel-react-native/package.json";
+
+/**
+ * JavaScript implementation of Feature Flags for React Native
+ * This is used when native modules are not available (Expo, React Native Web)
+ * Aligned with mixpanel-js reference implementation
+ */
+export class MixpanelFlagsJS {
+ constructor(token, mixpanelImpl, storage) {
+ this.token = token;
+ this.mixpanelImpl = mixpanelImpl;
+ this.storage = storage;
+ this.flags = new Map(); // Use Map like mixpanel-js
+ this.flagsReady = false;
+ this.experimentTracked = new Set();
+ this.context = {};
+ this.flagsCacheKey = `MIXPANEL_${token}_FLAGS_CACHE`;
+ this.flagsReadyKey = `MIXPANEL_${token}_FLAGS_READY`;
+ this.mixpanelPersistent = MixpanelPersistent.getInstance(storage, token);
+
+ // Performance tracking (mixpanel-js alignment)
+ this._fetchStartTime = null;
+ this._fetchCompleteTime = null;
+ this._fetchLatency = null;
+ this._traceparent = null;
+
+ // Load cached flags on initialization (fire and forget - loads in background)
+ // This is async but intentionally not awaited to avoid blocking constructor
+ // Flags will be available once cache loads or after explicit loadFlags() call
+ this.loadCachedFlags().catch(error => {
+ MixpanelLogger.log(this.token, "Failed to load cached flags in constructor:", error);
+ });
+ }
+
+ /**
+ * Load cached flags from storage
+ */
+ async loadCachedFlags() {
+ try {
+ const cachedFlags = await this.storage.getItem(this.flagsCacheKey);
+ if (cachedFlags) {
+ const parsed = JSON.parse(cachedFlags);
+ // Convert array back to Map for consistency
+ this.flags = new Map(parsed);
+ this.flagsReady = true;
+ MixpanelLogger.log(this.token, "Loaded cached feature flags");
+ }
+ } catch (error) {
+ MixpanelLogger.log(this.token, "Error loading cached flags:", error);
+ }
+ }
+
+ /**
+ * Cache flags to storage
+ */
+ async cacheFlags() {
+ try {
+ // Convert Map to array for JSON serialization
+ const flagsArray = Array.from(this.flags.entries());
+ await this.storage.setItem(
+ this.flagsCacheKey,
+ JSON.stringify(flagsArray)
+ );
+ await this.storage.setItem(this.flagsReadyKey, "true");
+ } catch (error) {
+ MixpanelLogger.log(this.token, "Error caching flags:", error);
+ }
+ }
+
+ /**
+ * Generate W3C traceparent header
+ * Format: 00-{traceID}-{parentID}-{flags}
+ * Returns null if UUID generation fails (graceful degradation)
+ */
+ generateTraceparent() {
+ try {
+ // Try expo-crypto first
+ const crypto = require("expo-crypto");
+ const traceID = crypto.randomUUID().replace(/-/g, "");
+ const parentID = crypto.randomUUID().replace(/-/g, "").substring(0, 16);
+ return `00-${traceID}-${parentID}-01`;
+ } catch (expoCryptoError) {
+ try {
+ // Fallback to uuid (import the v4 function directly)
+ const { v4: uuidv4 } = require("uuid");
+ const traceID = uuidv4().replace(/-/g, "");
+ const parentID = uuidv4().replace(/-/g, "").substring(0, 16);
+ return `00-${traceID}-${parentID}-01`;
+ } catch (uuidError) {
+ // Graceful degradation: traceparent is optional for observability
+ // Don't block flag loading if UUID generation fails
+ MixpanelLogger.log(
+ this.token,
+ "Could not generate traceparent (UUID unavailable):",
+ uuidError
+ );
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Mark fetch operation complete and calculate latency
+ */
+ markFetchComplete() {
+ if (!this._fetchStartTime) {
+ MixpanelLogger.error(
+ this.token,
+ "Fetch start time not set, cannot mark fetch complete"
+ );
+ return;
+ }
+ this._fetchCompleteTime = Date.now();
+ this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
+ this._fetchStartTime = null;
+ }
+
+ /**
+ * Fetch feature flags from Mixpanel API
+ */
+ async loadFlags() {
+ this._fetchStartTime = Date.now();
+
+ // Generate traceparent if possible (graceful degradation if UUID unavailable)
+ try {
+ this._traceparent = this.generateTraceparent();
+ } catch (error) {
+ // Silently skip traceparent if generation fails
+ this._traceparent = null;
+ }
+
+ try {
+ const distinctId = this.mixpanelPersistent.getDistinctId(this.token);
+ const deviceId = this.mixpanelPersistent.getDeviceId(this.token);
+
+ // Build context object (mixpanel-js format)
+ const context = {
+ distinct_id: distinctId,
+ device_id: deviceId,
+ ...this.context,
+ };
+
+ // Build query parameters (mixpanel-js format)
+ const queryParams = new URLSearchParams();
+ queryParams.set('context', JSON.stringify(context));
+ queryParams.set('token', this.token);
+ queryParams.set('mp_lib', 'react-native');
+ queryParams.set('$lib_version', packageJson.version);
+
+ MixpanelLogger.log(
+ this.token,
+ "Fetching feature flags with context:",
+ context
+ );
+
+ const serverURL =
+ this.mixpanelImpl.config?.getServerURL?.(this.token) ||
+ "https://api.mixpanel.com";
+
+ // Use /flags endpoint with query parameters (mixpanel-js format)
+ const endpoint = `/flags?${queryParams.toString()}`;
+
+ const response = await MixpanelNetwork.sendRequest({
+ token: this.token,
+ endpoint: endpoint,
+ data: null, // Data is in query params for flags endpoint
+ serverURL: serverURL,
+ useIPAddressForGeoLocation: true,
+ });
+
+ this.markFetchComplete();
+
+ // Support both response formats for backwards compatibility
+ if (response && response.flags) {
+ // New format (mixpanel-js compatible): {flags: {key: {variant_key, variant_value, ...}}}
+ this.flags = new Map();
+ for (const [key, data] of Object.entries(response.flags)) {
+ this.flags.set(key, {
+ key: data.variant_key,
+ value: data.variant_value,
+ experiment_id: data.experiment_id,
+ is_experiment_active: data.is_experiment_active,
+ is_qa_tester: data.is_qa_tester,
+ });
+ }
+ this.flagsReady = true;
+ await this.cacheFlags();
+ MixpanelLogger.log(this.token, "Feature flags loaded successfully");
+ } else if (response && response.featureFlags) {
+ // Legacy format: {featureFlags: [{key, value, experimentID, ...}]}
+ this.flags = new Map();
+ for (const flag of response.featureFlags) {
+ this.flags.set(flag.key, {
+ key: flag.key,
+ value: flag.value,
+ experiment_id: flag.experimentID,
+ is_experiment_active: flag.isExperimentActive,
+ is_qa_tester: flag.isQATester,
+ });
+ }
+ this.flagsReady = true;
+ await this.cacheFlags();
+ MixpanelLogger.warn(
+ this.token,
+ 'Received legacy featureFlags format. Please update backend to use "flags" format.'
+ );
+ }
+ } catch (error) {
+ this.markFetchComplete();
+ MixpanelLogger.log(this.token, "Error loading feature flags:", error);
+ // Keep using cached flags if available
+ if (this.flags.size > 0) {
+ this.flagsReady = true;
+ }
+ }
+ }
+
+ /**
+ * Check if flags are ready to use
+ */
+ areFlagsReady() {
+ return this.flagsReady;
+ }
+
+ /**
+ * Track experiment started event
+ * Aligned with mixpanel-js tracking properties
+ */
+ async trackExperimentStarted(featureName, variant) {
+ if (this.experimentTracked.has(featureName)) {
+ return; // Already tracked
+ }
+
+ try {
+ const properties = {
+ "Experiment name": featureName, // Human-readable (mixpanel-js format)
+ "Variant name": variant.key, // Human-readable (mixpanel-js format)
+ $experiment_type: "feature_flag", // Added to match mixpanel-js
+ };
+
+ // Add performance metrics if available
+ if (this._fetchCompleteTime) {
+ const fetchStartTime =
+ this._fetchCompleteTime - (this._fetchLatency || 0);
+ properties["Variant fetch start time"] = new Date(
+ fetchStartTime
+ ).toISOString();
+ properties["Variant fetch complete time"] = new Date(
+ this._fetchCompleteTime
+ ).toISOString();
+ properties["Variant fetch latency (ms)"] = this._fetchLatency || 0;
+ }
+
+ // Add traceparent if available
+ if (this._traceparent) {
+ properties["Variant fetch traceparent"] = this._traceparent;
+ }
+
+ // Add experiment metadata (system properties)
+ if (
+ variant.experiment_id !== undefined &&
+ variant.experiment_id !== null
+ ) {
+ properties["$experiment_id"] = variant.experiment_id;
+ }
+ if (variant.is_experiment_active !== undefined) {
+ properties["$is_experiment_active"] = variant.is_experiment_active;
+ }
+ if (variant.is_qa_tester !== undefined) {
+ properties["$is_qa_tester"] = variant.is_qa_tester;
+ }
+
+ // Track the experiment started event
+ await this.mixpanelImpl.track(
+ this.token,
+ "$experiment_started",
+ properties
+ );
+ this.experimentTracked.add(featureName);
+
+ MixpanelLogger.log(
+ this.token,
+ `Tracked experiment started for ${featureName}`
+ );
+ } catch (error) {
+ MixpanelLogger.log(this.token, "Error tracking experiment:", error);
+ }
+ }
+
+ /**
+ * Get variant synchronously (only works when flags are ready)
+ */
+ getVariantSync(featureName, fallback) {
+ if (!this.flagsReady || !this.flags.has(featureName)) {
+ return fallback;
+ }
+
+ const variant = this.flags.get(featureName);
+
+ // Track experiment on first access (fire and forget)
+ if (!this.experimentTracked.has(featureName)) {
+ this.trackExperimentStarted(featureName, variant).catch(() => {});
+ }
+
+ return variant;
+ }
+
+ /**
+ * Get variant value synchronously
+ */
+ getVariantValueSync(featureName, fallbackValue) {
+ const variant = this.getVariantSync(featureName, {
+ key: featureName,
+ value: fallbackValue,
+ });
+ return variant.value;
+ }
+
+ /**
+ * Check if feature is enabled synchronously
+ * Enhanced with boolean validation like mixpanel-js
+ */
+ isEnabledSync(featureName, fallbackValue = false) {
+ const value = this.getVariantValueSync(featureName, fallbackValue);
+
+ // Validate boolean type (mixpanel-js pattern)
+ if (value !== true && value !== false) {
+ MixpanelLogger.error(
+ this.token,
+ `Feature flag "${featureName}" value: ${value} is not a boolean; returning fallback value: ${fallbackValue}`
+ );
+ return fallbackValue;
+ }
+
+ return value;
+ }
+
+ /**
+ * Get variant asynchronously
+ */
+ async getVariant(featureName, fallback) {
+ // If flags not ready, try to load them
+ if (!this.flagsReady) {
+ await this.loadFlags();
+ }
+
+ if (!this.flags.has(featureName)) {
+ return fallback;
+ }
+
+ const variant = this.flags.get(featureName);
+
+ // Track experiment on first access
+ if (!this.experimentTracked.has(featureName)) {
+ await this.trackExperimentStarted(featureName, variant);
+ }
+
+ return variant;
+ }
+
+ /**
+ * Get variant value asynchronously
+ */
+ async getVariantValue(featureName, fallbackValue) {
+ const variant = await this.getVariant(featureName, {
+ key: featureName,
+ value: fallbackValue,
+ });
+ return variant.value;
+ }
+
+ /**
+ * Check if feature is enabled asynchronously
+ */
+ async isEnabled(featureName, fallbackValue = false) {
+ const value = await this.getVariantValue(featureName, fallbackValue);
+ if (typeof value === "boolean") {
+ return value;
+ } else {
+ MixpanelLogger.log(this.token, `Flag "${featureName}" value is not boolean:`, value);
+ return fallbackValue;
+ }
+ }
+
+ /**
+ * Update context and reload flags
+ * Aligned with mixpanel-js API signature
+ * @param {object} newContext - New context properties to add/update
+ * @param {object} options - Options object
+ * @param {boolean} options.replace - If true, replace entire context instead of merging
+ */
+ async updateContext(newContext, options = {}) {
+ if (options.replace) {
+ // Replace entire context
+ this.context = { ...newContext };
+ } else {
+ // Merge with existing context (default)
+ this.context = {
+ ...this.context,
+ ...newContext,
+ };
+ }
+
+ // Clear experiment tracking since context changed
+ this.experimentTracked.clear();
+
+ // Reload flags with new context
+ await this.loadFlags();
+
+ MixpanelLogger.log(this.token, "Context updated, flags reloaded");
+ }
+
+ /**
+ * Clear cached flags
+ */
+ async clearCache() {
+ try {
+ await this.storage.removeItem(this.flagsCacheKey);
+ await this.storage.removeItem(this.flagsReadyKey);
+ this.flags = new Map();
+ this.flagsReady = false;
+ this.experimentTracked.clear();
+ } catch (error) {
+ MixpanelLogger.log(this.token, "Error clearing flag cache:", error);
+ }
+ }
+
+ // snake_case aliases for API consistency with mixpanel-js
+ are_flags_ready() {
+ return this.areFlagsReady();
+ }
+
+ get_variant(featureName, fallback) {
+ return this.getVariant(featureName, fallback);
+ }
+
+ get_variant_sync(featureName, fallback) {
+ return this.getVariantSync(featureName, fallback);
+ }
+
+ get_variant_value(featureName, fallbackValue) {
+ return this.getVariantValue(featureName, fallbackValue);
+ }
+
+ get_variant_value_sync(featureName, fallbackValue) {
+ return this.getVariantValueSync(featureName, fallbackValue);
+ }
+
+ is_enabled(featureName, fallbackValue = false) {
+ return this.isEnabled(featureName, fallbackValue);
+ }
+
+ is_enabled_sync(featureName, fallbackValue = false) {
+ return this.isEnabledSync(featureName, fallbackValue);
+ }
+
+ update_context(newContext, options) {
+ return this.updateContext(newContext, options);
+ }
+}
diff --git a/javascript/mixpanel-flags.js b/javascript/mixpanel-flags.js
new file mode 100644
index 00000000..103a0cc8
--- /dev/null
+++ b/javascript/mixpanel-flags.js
@@ -0,0 +1,670 @@
+import { MixpanelFlagsJS } from './mixpanel-flags-js';
+
+/**
+ * Core class for using Mixpanel Feature Flags.
+ *
+ * The Flags class provides access to Mixpanel's Feature Flags functionality, enabling
+ * dynamic feature control, A/B testing, and personalized user experiences. Feature flags
+ * allow you to remotely configure your app's features without deploying new code.
+ *
+ *
This class is accessed through the {@link Mixpanel#flags} property and is lazy-loaded
+ * to minimize performance impact until feature flags are actually used.
+ *
+ *
Platform Support:
+ *
+ * - Native Mode (iOS/Android): Fully supported with automatic experiment tracking
+ * - JavaScript Mode (Expo/React Native Web): Planned for future release
+ *
+ *
+ * Key Concepts:
+ *
+ * - Feature Name: The unique identifier for your feature flag (e.g., "new-checkout")
+ * - Variant: An object containing both a key and value representing the feature configuration
+ * - Variant Key: The identifier for the specific variation (e.g., "control", "treatment")
+ * - Variant Value: The actual configuration value (can be any JSON-serializable type)
+ * - Fallback: Default value returned when a flag is not available or not loaded
+ *
+ *
+ * Automatic Experiment Tracking: When a feature flag is evaluated for the first time,
+ * Mixpanel automatically tracks a "$experiment_started" event with relevant metadata.
+ *
+ * @example
+ * // Initialize with feature flags enabled
+ * const mixpanel = new Mixpanel('YOUR_TOKEN', true);
+ * await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, {
+ * enabled: true,
+ * context: { platform: 'mobile' }
+ * });
+ *
+ * @example
+ * // Synchronous access (when flags are ready)
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-feature', false);
+ * const color = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ * const variant = mixpanel.flags.getVariantSync('checkout-flow', {
+ * key: 'control',
+ * value: 'standard'
+ * });
+ * }
+ *
+ * @example
+ * // Asynchronous access with Promise pattern
+ * const variant = await mixpanel.flags.getVariant('pricing-test', {
+ * key: 'control',
+ * value: { price: 9.99, currency: 'USD' }
+ * });
+ *
+ * @example
+ * // Asynchronous access with callback pattern
+ * mixpanel.flags.isEnabled('beta-features', false, (isEnabled) => {
+ * if (isEnabled) {
+ * // Enable beta features
+ * }
+ * });
+ *
+ * @see Mixpanel#flags
+ */
+export class Flags {
+ constructor(token, mixpanelImpl, storage) {
+ this.token = token;
+ this.mixpanelImpl = mixpanelImpl;
+ this.storage = storage;
+ this.isNativeMode = typeof mixpanelImpl.loadFlags === 'function';
+
+ // For JavaScript mode, create the JS implementation
+ if (!this.isNativeMode && storage) {
+ this.jsFlags = new MixpanelFlagsJS(token, mixpanelImpl, storage);
+ }
+ }
+
+ /**
+ * Manually fetch feature flags from the Mixpanel servers.
+ *
+ *
Feature flags are automatically loaded during initialization when feature flags are enabled.
+ * This method allows you to manually trigger a refresh of the flags, which is useful when:
+ *
+ * - You want to reload flags after a user property change
+ * - You need to ensure you have the latest flag configuration
+ * - Initial automatic load failed and you want to retry
+ *
+ *
+ * After successfully loading flags, {@link areFlagsReady} will return true and synchronous
+ * methods can be used to access flag values.
+ *
+ * @returns {Promise} A promise that resolves when flags have been fetched and loaded
+ * @throws {Error} if feature flags are not initialized
+ *
+ * @example
+ * // Manually reload flags after user identification
+ * await mixpanel.identify('user123');
+ * await mixpanel.flags.loadFlags();
+ */
+ async loadFlags() {
+ if (this.isNativeMode) {
+ return await this.mixpanelImpl.loadFlags(this.token);
+ } else if (this.jsFlags) {
+ return await this.jsFlags.loadFlags();
+ }
+ throw new Error("Feature flags are not initialized");
+ }
+
+ /**
+ * Check if feature flags have been fetched from the server and are ready to use.
+ *
+ * This method returns true after feature flags have been successfully loaded via {@link loadFlags}
+ * or during initialization. When flags are ready, you can safely use the synchronous methods
+ * ({@link getVariantSync}, {@link getVariantValueSync}, {@link isEnabledSync}) without waiting.
+ *
+ *
It's recommended to check this before using synchronous methods to ensure you're not
+ * getting fallback values due to flags not being loaded yet.
+ *
+ * @returns {boolean} true if flags have been loaded and are ready to use, false otherwise
+ *
+ * @example
+ * // Check before using synchronous methods
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const isEnabled = mixpanel.flags.isEnabledSync('new-feature', false);
+ * } else {
+ * console.log('Flags not ready yet, using fallback');
+ * }
+ *
+ * @example
+ * // Wait for flags to be ready
+ * await mixpanel.flags.loadFlags();
+ * if (mixpanel.flags.areFlagsReady()) {
+ * // Now safe to use sync methods
+ * }
+ */
+ areFlagsReady() {
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.areFlagsReadySync(this.token);
+ } else if (this.jsFlags) {
+ return this.jsFlags.areFlagsReady();
+ }
+ return false;
+ }
+
+ /**
+ * Get a feature flag variant synchronously.
+ *
+ *
Returns the complete variant object for a feature flag, including both the variant key
+ * (e.g., "control", "treatment") and the variant value (the actual configuration data).
+ *
+ *
Important: This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link getVariant} method instead.
+ *
+ *
When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {object} fallback The fallback variant object to return if the flag is not available.
+ * Must include both 'key' and 'value' properties.
+ * @returns {object} The flag variant object with the following structure:
+ * - key: {string} The variant key (e.g., "control", "treatment")
+ * - value: {any} The variant value (can be any JSON-serializable type)
+ * - experiment_id: {string|number} (optional) The experiment ID if this is an experiment
+ * - is_experiment_active: {boolean} (optional) Whether the experiment is currently active
+ *
+ * @example
+ * // Get a checkout flow variant
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const variant = mixpanel.flags.getVariantSync('checkout-flow', {
+ * key: 'control',
+ * value: 'standard'
+ * });
+ * console.log(`Using variant: ${variant.key}`);
+ * console.log(`Configuration: ${JSON.stringify(variant.value)}`);
+ * }
+ *
+ * @example
+ * // Get a complex configuration variant
+ * const defaultConfig = {
+ * key: 'default',
+ * value: {
+ * theme: 'light',
+ * layout: 'grid',
+ * itemsPerPage: 20
+ * }
+ * };
+ * const config = mixpanel.flags.getVariantSync('ui-config', defaultConfig);
+ *
+ * @see getVariant for asynchronous access
+ * @see getVariantValueSync to get only the value (not the full variant object)
+ */
+ getVariantSync(featureName, fallback) {
+ if (!this.areFlagsReady()) {
+ return fallback;
+ }
+
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.getVariantSync(this.token, featureName, fallback);
+ } else if (this.jsFlags) {
+ return this.jsFlags.getVariantSync(featureName, fallback);
+ }
+ return fallback;
+ }
+
+ /**
+ * Get a feature flag variant value synchronously.
+ *
+ *
Returns only the value portion of a feature flag variant, without the variant key or metadata.
+ * This is useful when you only care about the configuration data, not which variant was selected.
+ *
+ *
Important: This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link getVariantValue} method instead.
+ *
+ *
When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {any} fallbackValue The fallback value to return if the flag is not available.
+ * Can be any JSON-serializable type (string, number, boolean, object, array, etc.)
+ * @returns {any} The flag's value, or the fallback if the flag is not available.
+ * The return type matches the type of value configured in your Mixpanel project.
+ *
+ * @example
+ * // Get a simple string value
+ * if (mixpanel.flags.areFlagsReady()) {
+ * const buttonColor = mixpanel.flags.getVariantValueSync('button-color', 'blue');
+ * applyButtonColor(buttonColor);
+ * }
+ *
+ * @example
+ * // Get a complex object value
+ * const defaultPricing = { price: 9.99, currency: 'USD', trial_days: 7 };
+ * const pricing = mixpanel.flags.getVariantValueSync('pricing-config', defaultPricing);
+ * console.log(`Price: ${pricing.price} ${pricing.currency}`);
+ *
+ * @example
+ * // Get a boolean value
+ * const showPromo = mixpanel.flags.getVariantValueSync('show-promo', false);
+ * if (showPromo) {
+ * displayPromotionalBanner();
+ * }
+ *
+ * @see getVariantValue for asynchronous access
+ * @see getVariantSync to get the full variant object including key and metadata
+ */
+ getVariantValueSync(featureName, fallbackValue) {
+ if (!this.areFlagsReady()) {
+ return fallbackValue;
+ }
+
+ if (this.isNativeMode) {
+ // Android returns a wrapped object due to React Native limitations
+ const result = this.mixpanelImpl.getVariantValueSync(this.token, featureName, fallbackValue);
+ if (result && typeof result === 'object' && 'type' in result) {
+ // Android wraps the response
+ return result.type === 'fallback' ? fallbackValue : result.value;
+ }
+ // iOS returns the value directly
+ return result;
+ } else if (this.jsFlags) {
+ return this.jsFlags.getVariantValueSync(featureName, fallbackValue);
+ }
+ return fallbackValue;
+ }
+
+ /**
+ * Check if a feature flag is enabled synchronously.
+ *
+ *
This is a convenience method for boolean feature flags. It checks if a feature is enabled
+ * by evaluating the variant value as a boolean. A feature is considered "enabled" when its
+ * variant value evaluates to true.
+ *
+ *
Important: This is a synchronous method that only works when flags are ready.
+ * Always check {@link areFlagsReady} first, or use the asynchronous {@link isEnabled} method instead.
+ *
+ *
When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {boolean} [fallbackValue=false] The fallback value to return if the flag is not available.
+ * Defaults to false if not provided.
+ * @returns {boolean} true if the feature is enabled, false otherwise
+ *
+ * @example
+ * // Simple feature toggle
+ * if (mixpanel.flags.areFlagsReady()) {
+ * if (mixpanel.flags.isEnabledSync('new-checkout', false)) {
+ * showNewCheckout();
+ * } else {
+ * showLegacyCheckout();
+ * }
+ * }
+ *
+ * @example
+ * // With explicit fallback
+ * const enableBetaFeatures = mixpanel.flags.isEnabledSync('beta-features', true);
+ *
+ * @see isEnabled for asynchronous access
+ * @see getVariantValueSync for non-boolean flag values
+ */
+ isEnabledSync(featureName, fallbackValue = false) {
+ if (!this.areFlagsReady()) {
+ return fallbackValue;
+ }
+
+ if (this.isNativeMode) {
+ return this.mixpanelImpl.isEnabledSync(this.token, featureName, fallbackValue);
+ } else if (this.jsFlags) {
+ return this.jsFlags.isEnabledSync(featureName, fallbackValue);
+ }
+ return fallbackValue;
+ }
+
+ /**
+ * Get a feature flag variant asynchronously.
+ *
+ *
Returns the complete variant object for a feature flag, including both the variant key
+ * and the variant value. This method works regardless of whether flags are ready, making it
+ * safe to use at any time.
+ *
+ *
Supports both Promise and callback patterns for maximum flexibility.
+ *
+ *
When a flag is evaluated for the first time, Mixpanel automatically tracks a
+ * "$experiment_started" event with relevant experiment metadata.
+ *
+ * @param {string} featureName The unique identifier for the feature flag
+ * @param {object} fallback The fallback variant object to return if the flag is not available.
+ * Must include both 'key' and 'value' properties.
+ * @param {function} [callback] Optional callback function that receives the variant object.
+ * If provided, the method returns void. If omitted, the method returns a Promise.
+ * @returns {Promise