July 28, 2026 - open-feature-v0.2.0
An OpenFeature provider that wraps Mixpanel's feature flags for use with the OpenFeature Kotlin (Android) SDK. This allows you to use Mixpanel's feature flagging capabilities through OpenFeature's standardized, vendor-agnostic API.
This package provides a bridge between Mixpanel's native feature flags implementation and the OpenFeature specification. By using this provider, you can:
- Leverage Mixpanel's powerful feature flag and experimentation platform
- Use OpenFeature's standardized API for flag evaluation
- Easily switch between feature flag providers without changing your application code
- Integrate with OpenFeature's ecosystem of tools and frameworks
Add the following dependencies to your app's build.gradle.kts:
dependencies {
implementation("com.mixpanel.android:mixpanel-android-openfeature:<version>")
implementation("dev.openfeature:kotlin-sdk-android:0.7.2")
}Or with Groovy (build.gradle):
dependencies {
implementation 'com.mixpanel.android:mixpanel-android-openfeature:<version>'
implementation 'dev.openfeature:kotlin-sdk-android:0.7.2'
}import com.mixpanel.android.openfeature.MixpanelProvider
import com.mixpanel.android.mpmetrics.MixpanelOptions
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
// 1. Create the provider using the convenience constructor
val options = MixpanelOptions.Builder()
.featureFlags()
.build()
val provider = MixpanelProvider(context, "YOUR_PROJECT_TOKEN", options)
// 2. Register the provider with OpenFeature
OpenFeatureAPI.setProviderAndWait(provider)
// 3. Get a client and evaluate flags
val client = OpenFeatureAPI.getClient()
val showNewFeature = client.getBooleanValue("new-feature-flag", false)
if (showNewFeature) {
// New feature is enabled!
}If you already have a MixpanelAPI instance, you can pass its Flags object directly:
import com.mixpanel.android.mpmetrics.MixpanelAPI
import com.mixpanel.android.openfeature.MixpanelProvider
val mixpanel = MixpanelAPI.getInstance(context, "YOUR_PROJECT_TOKEN", false, options)
val provider = MixpanelProvider(mixpanel.getFlags())
OpenFeatureAPI.setProviderAndWait(provider)Important: If you need to call
mixpanel.identify()to update the logged-in user ormixpanel.track()to use Runtime Events for targeting, you must call these methods on the sameMixpanelAPIinstance whoseFlagsobject was passed to the provider. Using a different instance will not affect the provider's flag evaluation.
When using the convenience constructor, the underlying instance is accessible via provider.mixpanel:
val provider = MixpanelProvider(context, "YOUR_PROJECT_TOKEN", options)
// Use the same instance for identity and tracking
provider.mixpanel?.identify("user-123")
provider.mixpanel?.track("Purchase Completed")val client = OpenFeatureAPI.getClient()
// Get a boolean flag with a default value
val isFeatureEnabled = client.getBooleanValue("my-feature", false)
if (isFeatureEnabled) {
// Show the new feature
}Mixpanel feature flags support three flag types. Use the corresponding OpenFeature evaluation method based on your flag's variant values:
| Mixpanel Flag Type | Variant Values | OpenFeature Method |
|---|---|---|
| Feature Gate | true / false |
getBooleanValue() |
| Experiment | boolean, string, number, or JSON object | getBooleanValue(), getStringValue(), getIntegerValue(), getDoubleValue(), or getObjectValue() |
| Dynamic Config | JSON object | getObjectValue() |
val client = OpenFeatureAPI.getClient()
// Feature Gate - boolean variants
val isFeatureOn = client.getBooleanValue("new-checkout", false)
// Experiment with string variants
val buttonColor = client.getStringValue("button-color-test", "blue")
// Experiment with integer variants
val maxItems = client.getIntegerValue("max-items", 10)
// Experiment with double variants
val threshold = client.getDoubleValue("score-threshold", 0.5)
// Dynamic Config - JSON object variants
val featureConfig = client.getObjectValue("homepage-layout", Value.Structure(mapOf(
"layout" to Value.String("grid"),
"itemsPerRow" to Value.Integer(3)
)))If you need additional metadata about the flag evaluation:
val client = OpenFeatureAPI.getClient()
val details = client.getBooleanDetails("my-feature", false)
println(details.value) // The resolved value
println(details.variant) // The variant key from Mixpanel
println(details.reason) // Why this value was returned
println(details.errorCode) // Error code if evaluation failedYou can pass evaluation context that will be sent to Mixpanel for flag evaluation using OpenFeatureAPI.setContext():
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.Value
OpenFeatureAPI.setContext(ImmutableContext(
attributes = mutableMapOf(
"email" to Value.String("user@example.com"),
"plan" to Value.String("premium")
)
))Note: Per-evaluation context (the optional
contextparameter on evaluation methods) is not supported by this provider. Context must be set globally viaOpenFeatureAPI.setContext(), which triggers a re-fetch of flag values from Mixpanel.
You can pass custom_properties in the evaluation context for use with Mixpanel's Runtime Properties targeting rules. Values must be flat key-value pairs (no nested objects):
OpenFeatureAPI.setContext(ImmutableContext(
attributes = mutableMapOf(
"custom_properties" to Value.Structure(mapOf(
"tier" to Value.String("enterprise"),
"seats" to Value.Integer(50),
"industry" to Value.String("technology")
))
)
))Understanding how OpenFeature context maps to Mixpanel:
All properties in the OpenFeature EvaluationContext are passed directly to Mixpanel's feature flag evaluation. There is no transformation or filtering of properties.
// This OpenFeature context...
OpenFeatureAPI.setContext(ImmutableContext(
targetingKey = "user-123",
attributes = mutableMapOf(
"email" to Value.String("user@example.com"),
"plan" to Value.String("premium"),
"beta_tester" to Value.Boolean(true)
)
))
// ...is passed to Mixpanel as-is for flag evaluationUnlike some feature flag providers, targetingKey is not used as a special bucketing key in Mixpanel. It is simply passed as another context property. Mixpanel's server-side configuration determines which properties are used for:
- Targeting rules: Which users see which variants
- Bucketing: How users are consistently assigned to variants
Important: This provider does not call mixpanel.identify(). User identity should be managed separately through the same Mixpanel instance that backs this provider:
// Manage identity through the same Mixpanel instance
provider.mixpanel?.identify("user-123")
// The provider will use Mixpanel's current distinct_id automatically
val client = OpenFeatureAPI.getClient()
val value = client.getBooleanValue("my-flag", false)The main provider class that implements the OpenFeature FeatureProvider interface.
// Primary constructor - pass a Flags instance directly
MixpanelProvider(flags: MixpanelAPI.Flags)
// Convenience constructor - creates a MixpanelAPI instance internally
MixpanelProvider(context: Context, token: String, options: MixpanelOptions)| Property | Type | Description |
|---|---|---|
metadata |
ProviderMetadata |
Provider metadata with the name "mixpanel-provider" |
mixpanel |
MixpanelAPI? |
The underlying Mixpanel instance (only available when using the convenience constructor) |
| Method | Description |
|---|---|
initialize(initialContext?) |
Called when the provider is registered. Applies initial context if provided. |
onContextSet(oldContext, newContext) |
Called when the global evaluation context changes. Sends updated context to Mixpanel. |
shutdown() |
Called when the provider is shut down. No-op since Mixpanel manages its own lifecycle. |
getBooleanEvaluation(key, defaultValue, context) |
Evaluates a boolean flag |
getStringEvaluation(key, defaultValue, context) |
Evaluates a string flag |
getIntegerEvaluation(key, defaultValue, context) |
Evaluates an integer flag |
getDoubleEvaluation(key, defaultValue, context) |
Evaluates a double flag |
getObjectEvaluation(key, defaultValue, context) |
Evaluates an object flag |
The provider uses OpenFeature's standard error codes to indicate issues during flag evaluation:
Returned when flags are evaluated before the provider has finished initializing.
// To avoid this error, use setProviderAndWait
OpenFeatureAPI.setProviderAndWait(provider)
// Or listen for the READY event
OpenFeatureAPI.observe<OpenFeatureEvents.ProviderReady> {
// Now safe to evaluate flags
}Returned when the requested flag does not exist in Mixpanel.
val details = client.getBooleanDetails("nonexistent-flag", false)
if (details.errorCode == ErrorCode.FLAG_NOT_FOUND) {
println("Flag does not exist, using default value")
}Returned when the flag value type does not match the requested type.
// If 'my-flag' is configured as a string in Mixpanel...
val details = client.getBooleanDetails("my-flag", false)
if (details.errorCode == ErrorCode.TYPE_MISMATCH) {
println("Flag is not a boolean, using default value")
}Possible causes:
-
Feature flags not enabled: Ensure you configured Mixpanel with feature flags enabled:
val options = MixpanelOptions.Builder() .featureFlags() .build()
-
Provider not ready: Make sure to wait for the provider to initialize:
OpenFeatureAPI.setProviderAndWait(provider) -
Network issues: Check Logcat for failed requests to Mixpanel's flags API.
-
Flag not configured: Verify the flag exists in your Mixpanel project and is enabled.
If you are getting TYPE_MISMATCH errors:
-
Check flag configuration: Verify the flag's value type in Mixpanel matches how you are evaluating it:
// If flag value is a string like "true", use getStringValue, not getBooleanValue val value = client.getStringValue("my-flag", "default")
-
Use getObjectValue for complex types: For JSON objects or arrays, use
getObjectValue.
If $experiment_started events are not appearing in Mixpanel:
-
Verify Mixpanel tracking is working: Test that other Mixpanel events are being tracked successfully.
-
Check for duplicate evaluations: Mixpanel only tracks the first exposure per flag per session to avoid duplicate events.
When you update the OpenFeature context, the provider needs to fetch new flag values:
// Update context and wait for new flags
OpenFeatureAPI.setContext(ImmutableContext(
attributes = mutableMapOf("plan" to Value.String("premium"))
))
// Now evaluate with new context
val value = client.getBooleanValue("premium-feature", false)If flags still are not updating, check that your targeting rules in Mixpanel are configured to use the context properties you are setting.
Apache-2.0