Skip to content

Initial Feature Flags Support #859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.4'
classpath 'com.android.tools.build:gradle:8.10.0'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0'
}
}
Expand Down Expand Up @@ -56,7 +56,6 @@ android {
buildTypes {
debug{
minifyEnabled false
debuggable true
testCoverageEnabled true
}
release {
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.compose.*
import com.mixpanel.mixpaneldemo.ui.theme.MixpanelandroidTheme

const val MIXPANEL_PROJECT_TOKEN = "YOUR_PROJECT_TOKEN"
const val MIXPANEL_PROJECT_TOKEN = "metrics-1"

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
121 changes: 119 additions & 2 deletions mixpaneldemo/src/main/java/com/mixpanel/mixpaneldemo/TrackingPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,39 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.mixpanel.android.mpmetrics.FeatureFlagData
import com.mixpanel.android.mpmetrics.FlagCompletionCallback
import com.mixpanel.android.mpmetrics.MixpanelAPI
import com.mixpanel.android.mpmetrics.MixpanelOptions
import org.json.JSONObject

@Composable
fun TrackingPage(navController: NavHostController) {
val showDialog = remember { mutableStateOf(false) }
val dialogMessage = remember { mutableStateOf("") }
val context = LocalContext.current
val mixpanel = MixpanelAPI.getInstance(context, MIXPANEL_PROJECT_TOKEN, true)
val flagsContext = JSONObject()
flagsContext.put("context_key", "context_value")
val mpOptionsBuilder = MixpanelOptions.Builder().featureFlagsEnabled(true).featureFlagsContext(flagsContext)
val mpOptions = mpOptionsBuilder.build()
val mixpanel = MixpanelAPI.getInstance(context, MIXPANEL_PROJECT_TOKEN, true, mpOptions)
mixpanel.setEnableLogging(true)
mixpanel.setShouldGzipRequestPayload(true)
mixpanel.identify("demo_user")
// mixpanel.setShouldGzipRequestPayload(true)
mixpanel.setNetworkErrorListener(SimpleLoggingErrorListener())

// Define some flag keys and fallbacks for testing
val testFlagKeyString = "jared_android_test"
val testFlagKeyInt = "test-int-flag"
val testFlagKeyBool = "test-boolean-flag"
val testFlagKeyMissing = "missing-flag"

val stringFallback = FeatureFlagData(testFlagKeyString, "default-string") // Fallback Data object
val intFallbackData = FeatureFlagData(testFlagKeyInt, 0) // Fallback Data object
val boolFallbackData = FeatureFlagData(testFlagKeyBool, false) // Fallback Data object
val missingFallbackData = FeatureFlagData("missing-fallback-key", "missing-fallback-val")


val trackingActions = listOf(
Triple("Track w/o Properties" ,"Event: \"Track Event!\"", { println("Tracking without properties")
mixpanel.track("Track w/o Properties")
Expand Down Expand Up @@ -88,6 +108,103 @@ fun TrackingPage(navController: NavHostController) {
println("Unregistering super property")
mixpanel.unregisterSuperProperty("property_to_unregister")
mixpanel.flush()
}),
// --- Feature Flag Actions ---
Triple("Load Flags", "Action: Loading flags async...", {
println(">>> Action: Loading flags")
mixpanel.loadFlags()
// You might want to call getFeature async shortly after to see results
}),
Triple("Are Flags Ready?", "Action: Checking areFeaturesReady...", {
println(">>> Action: Checking areFeaturesReady")
val ready = mixpanel.areFeaturesReady()
println("areFeaturesReady Result: $ready")
// Update dialog or state if needed based on 'ready'
dialogMessage.value = "areFeaturesReady() returned: $ready"
showDialog.value = true
}),
Triple("Get Feature Sync", "Action: Getting flag '$testFlagKeyString' sync...", {
println(">>> Action: Getting feature sync '$testFlagKeyString'")
// Note: Fallback for getFeatureSync is FeatureFlagData type
val featureFlagData = mixpanel.getFeatureSync(testFlagKeyString, stringFallback)
println("Sync Get Feature Result for '$testFlagKeyString':")
println(" Key: ${featureFlagData.key}")
println(" Value: ${featureFlagData.value}")
// Also try a missing key
val missingData = mixpanel.getFeatureSync(testFlagKeyMissing, missingFallbackData)
println("Sync Get Feature Result for '$testFlagKeyMissing':")
println(" Key: ${missingData.key}")
println(" Value: ${missingData.value}")
}),
// --- NEW ---
Triple("Get Feature Async", "Action: Getting flag '$testFlagKeyString' async...", {
println(">>> Action: Getting feature async '$testFlagKeyString'")
mixpanel.getFeature(testFlagKeyString, stringFallback,
FlagCompletionCallback { result -> // Use SAM conversion for callback
println("Async Get Feature Result for '$testFlagKeyString':")
// FeatureFlagData result itself should not be null, but value inside can be
println(" Key: ${result.key}")
println(" Value: ${result.value}")
})
// Also try a missing key async
mixpanel.getFeature(testFlagKeyMissing, missingFallbackData,
FlagCompletionCallback { result ->
println("Async Get Feature Result for '$testFlagKeyMissing':")
println(" Key: ${result.key}")
println(" Value: ${result.value}")
})
}),
Triple("Get Flag Data Sync", "Action: Getting data for '$testFlagKeyInt' sync...", {
println(">>> Action: Getting feature data sync '$testFlagKeyInt'")
val fallbackValue = -1 // Fallback for getFeatureDataSync is the value type
val value = mixpanel.getFeatureDataSync(testFlagKeyInt, fallbackValue)
println("Sync Get Data Result for '$testFlagKeyInt': $value (Type: ${value?.javaClass?.simpleName})")
// Also try a missing key
val missingValue = mixpanel.getFeatureDataSync(testFlagKeyMissing, "missing_default")
println("Sync Get Data Result for '$testFlagKeyMissing': $missingValue")
}),
Triple("Get Flag Data Async", "Action: Getting data for '$testFlagKeyInt' async...", {
println(">>> Action: Getting feature data async '$testFlagKeyInt'")
val fallbackValue = -1
mixpanel.getFeatureData(testFlagKeyInt, fallbackValue,
FlagCompletionCallback { value -> // SAM conversion
println("Async Get Data Result for '$testFlagKeyInt': $value (Type: ${value?.javaClass?.simpleName})")
})
// Also try a missing key
mixpanel.getFeatureData(testFlagKeyMissing, "missing_default",
FlagCompletionCallback { value ->
println("Async Get Data Result for '$testFlagKeyMissing': $value")
})
}),
Triple("Is Enabled Sync", "Action: Checking '$testFlagKeyBool' sync...", {
println(">>> Action: Checking feature enabled sync '$testFlagKeyBool'")
val fallbackValue = false // Fallback for isEnabledSync is boolean
val isEnabled = mixpanel.isFeatureEnabledSync(testFlagKeyBool, fallbackValue)
println("Sync Is Enabled Result for '$testFlagKeyBool': $isEnabled")
// Also try a missing key
val isMissingEnabled = mixpanel.isFeatureEnabledSync(testFlagKeyMissing, true) // Use different fallback
println("Sync Is Enabled Result for '$testFlagKeyMissing': $isMissingEnabled")
// Try on a non-boolean flag
val isIntEnabled = mixpanel.isFeatureEnabledSync(testFlagKeyInt, true) // Should return fallback
println("Sync Is Enabled Result for '$testFlagKeyInt': $isIntEnabled")
}),
Triple("Is Enabled Async", "Action: Checking '$testFlagKeyBool' async...", {
println(">>> Action: Checking feature enabled async '$testFlagKeyBool'")
val fallbackValue = false
mixpanel.isFeatureEnabled(testFlagKeyBool, fallbackValue,
FlagCompletionCallback { isEnabled -> // SAM conversion
println("Async Is Enabled Result for '$testFlagKeyBool': $isEnabled")
})
// Also try a missing key
mixpanel.isFeatureEnabled(testFlagKeyMissing, true, // Use different fallback
FlagCompletionCallback { isEnabled ->
println("Async Is Enabled Result for '$testFlagKeyMissing': $isEnabled")
})
// Try on a non-boolean flag
mixpanel.isFeatureEnabled(testFlagKeyInt, true, // Should return fallback
FlagCompletionCallback { isEnabled ->
println("Async Is Enabled Result for '$testFlagKeyInt': $isEnabled")
})
})
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import android.os.HandlerThread;
import android.os.Process;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
Expand Down Expand Up @@ -63,7 +65,14 @@ public void setUp() {
private void setUpInstance(boolean trackAutomaticEvents) {
final RemoteService mockPoster = new HttpService() {
@Override
public byte[] performRequest(String endpointUrl, ProxyServerInteractor interactor, Map<String, Object> params, SSLSocketFactory socketFactory) {
public byte[] performRequest(
@NonNull String endpointUrl,
@Nullable ProxyServerInteractor interactor,
@Nullable Map<String, Object> params, // Used only if requestBodyBytes is null
@Nullable Map<String, String> headers,
@Nullable byte[] requestBodyBytes, // If provided, send this as raw body
@Nullable SSLSocketFactory socketFactory)
{

final String jsonData = Base64Coder.decodeString(params.get("data").toString());
assertTrue(params.containsKey("data"));
Expand Down Expand Up @@ -212,7 +221,14 @@ public void testAutomaticMultipleInstances() throws InterruptedException {

final HttpService mpSecondPoster = new HttpService() {
@Override
public byte[] performRequest(String endpointUrl, ProxyServerInteractor interactor, Map<String, Object> params, SSLSocketFactory socketFactory) {
public byte[] performRequest(
@NonNull String endpointUrl,
@Nullable ProxyServerInteractor interactor,
@Nullable Map<String, Object> params, // Used only if requestBodyBytes is null
@Nullable Map<String, String> headers,
@Nullable byte[] requestBodyBytes, // If provided, send this as raw body
@Nullable SSLSocketFactory socketFactory)
{
final String jsonData = Base64Coder.decodeString(params.get("data").toString());
assertTrue(params.containsKey("data"));
try {
Expand Down
Loading
Loading