diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java index 60b53577..f9a72ae8 100644 --- a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java +++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java @@ -303,6 +303,18 @@ private String createFlagsResponseJson(Map flags) { } else { flagDef.put("variant_value", entry.getValue().value); } + + // Add optional experiment parameters if they exist + if (entry.getValue().experimentID != null) { + flagDef.put("experiment_id", entry.getValue().experimentID); + } + if (entry.getValue().isExperimentActive != null) { + flagDef.put("is_experiment_active", entry.getValue().isExperimentActive); + } + if (entry.getValue().isQATester != null) { + flagDef.put("is_qa_tester", entry.getValue().isQATester); + } + flagsObject.put(entry.getKey(), flagDef); } return new JSONObject().put("flags", flagsObject).toString(); @@ -1701,4 +1713,38 @@ public void testTimingUpperBoundValidation() throws InterruptedException, JSONEx // In practice, our test should complete much faster assertTrue("Test fetch should be fast in practice", fetchLatencyMs < 5000); } + + @Test + public void testOptionalParametersInTracking_WithAllFields_ShouldIncludeInProperties() throws Exception { + // Create flag variant with all experiment parameters + Map serverFlags = new HashMap<>(); + serverFlags.put("test_tracking_flag", new MixpanelFlagVariant("variant_c", "tracking_value", "exp_789", true, true)); + + mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8)); + mFeatureFlagManager.loadFlags(); + + // Wait for flags to be ready + for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) { + Thread.sleep(100); + } + assertTrue("Flags should be ready", mFeatureFlagManager.areFlagsReady()); + + // Setup tracking expectation + mMockDelegate.resetTrackCalls(); + mMockDelegate.trackCalledLatch = new CountDownLatch(1); + + // Get variant to trigger tracking + MixpanelFlagVariant fallback = new MixpanelFlagVariant("fallback", "fallback_value"); + mFeatureFlagManager.getVariantSync("test_tracking_flag", fallback); + + // Wait for tracking call + assertTrue("Track should be called", mMockDelegate.trackCalledLatch.await(5, TimeUnit.SECONDS)); + assertEquals("Track should be called exactly once", 1, mMockDelegate.trackCalls.size()); + + // Verify tracking properties include optional parameters + MockFeatureFlagDelegate.TrackCall call = mMockDelegate.trackCalls.get(0); + assertEquals("ExperimentID should be included", "exp_789", call.properties.getString("$experiment_id")); + assertTrue("IsExperimentActive should be included", call.properties.getBoolean("$is_experiment_active")); + assertTrue("IsQATester should be included", call.properties.getBoolean("$is_qa_tester")); + } } diff --git a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java index f6548c5c..d2fc245a 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java @@ -711,6 +711,16 @@ private void _performTrackingDelegateCall(String flagName, MixpanelFlagVariant v properties.put("timeLastFetched", timing.timeLastFetched); properties.put("fetchLatencyMs", timing.fetchLatencyMs); } + + if (variant.experimentID != null) { + properties.put("$experiment_id", variant.experimentID); + } + if (variant.isExperimentActive != null) { + properties.put("$is_experiment_active", variant.isExperimentActive); + } + if (variant.isQATester != null) { + properties.put("$is_qa_tester", variant.isQATester); + } } catch (JSONException e) { MPLog.e(LOGTAG, "Failed to create JSON properties for $experiment_started event", e); return; // Don't track if properties failed diff --git a/src/main/java/com/mixpanel/android/mpmetrics/MixpanelFlagVariant.java b/src/main/java/com/mixpanel/android/mpmetrics/MixpanelFlagVariant.java index cc84b6f6..d152384c 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/MixpanelFlagVariant.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/MixpanelFlagVariant.java @@ -26,7 +26,25 @@ public class MixpanelFlagVariant { public final Object value; /** - * Constructs a {@code FeatureFlagData} object when parsing an API response. + * The value of experimentID. This corresponds to the optional 'experiment_id' field in the Mixpanel API response. + */ + @Nullable + public final String experimentID; + + /** + * The value of isExperimentActive. This corresponds to the optional 'is_experiment_active' field in the Mixpanel API response. + */ + @Nullable + public final Boolean isExperimentActive; + + /** + * The value of isQATester. This corresponds to the optional 'is_qa_tester' field in the Mixpanel API response. + */ + @Nullable + public final Boolean isQATester; + + /** + * Constructs a {@code MixpanelFlagVariant} object when parsing an API response. * * @param key The key of the feature flag variant. Corresponds to 'variant_key' from the API. Cannot be null. * @param value The value of the feature flag variant. Corresponds to 'variant_value' from the API. @@ -35,10 +53,31 @@ public class MixpanelFlagVariant { public MixpanelFlagVariant(@NonNull String key, @Nullable Object value) { this.key = key; this.value = value; + this.experimentID = null; + this.isExperimentActive = null; + this.isQATester = null; + } + + /** + * Constructs a {@code MixpanelFlagVariant} object when parsing an API response with optional experiment fields. + * + * @param key The key of the feature flag variant. Corresponds to 'variant_key' from the API. Cannot be null. + * @param value The value of the feature flag variant. Corresponds to 'variant_value' from the API. + * Can be Boolean, String, Number, JSONArray, JSONObject, or null. + * @param experimentID The experiment ID. Corresponds to 'experiment_id' from the API. Can be null. + * @param isExperimentActive Whether the experiment is active. Corresponds to 'is_experiment_active' from the API. Can be null. + * @param isQATester Whether the user is a QA tester. Corresponds to 'is_qa_tester' from the API. Can be null. + */ + public MixpanelFlagVariant(@NonNull String key, @Nullable Object value, @Nullable String experimentID, @Nullable Boolean isExperimentActive, @Nullable Boolean isQATester) { + this.key = key; + this.value = value; + this.experimentID = experimentID; + this.isExperimentActive = isExperimentActive; + this.isQATester = isQATester; } /** - * Constructs a {@code FeatureFlagData} object for creating fallback instances. + * Constructs a {@code MixpanelFlagVariant} object for creating fallback instances. * In this case, the provided {@code keyAndValue} is used as both the key and the value * for the feature flag data. This is typically used when a flag is not found * and a default string value needs to be returned. @@ -48,10 +87,13 @@ public MixpanelFlagVariant(@NonNull String key, @Nullable Object value) { public MixpanelFlagVariant(@NonNull String keyAndValue) { this.key = keyAndValue; // Default key to the value itself this.value = keyAndValue; + this.experimentID = null; + this.isExperimentActive = null; + this.isQATester = null; } /** - * Constructs a {@code FeatureFlagData} object for creating fallback instances. + * Constructs a {@code MixpanelFlagVariant} object for creating fallback instances. * In this version, the key is set to an empty string (""), and the provided {@code value} * is used as the value for the feature flag data. This is typically used when a * flag is not found or an error occurs, and a default value needs to be provided. @@ -62,15 +104,21 @@ public MixpanelFlagVariant(@NonNull String keyAndValue) { public MixpanelFlagVariant(@NonNull Object value) { this.key = ""; this.value = value; + this.experimentID = null; + this.isExperimentActive = null; + this.isQATester = null; } /** - * Default constructor that initializes an empty {@code FeatureFlagData} object. + * Default constructor that initializes an empty {@code MixpanelFlagVariant} object. * The key is set to an empty string ("") and the value is set to null. * This constructor might be used internally or for specific default cases. */ MixpanelFlagVariant() { this.key = ""; this.value = null; + this.experimentID = null; + this.isExperimentActive = null; + this.isQATester = null; } } \ No newline at end of file diff --git a/src/main/java/com/mixpanel/android/util/JsonUtils.java b/src/main/java/com/mixpanel/android/util/JsonUtils.java index 1ea09814..7393fab8 100644 --- a/src/main/java/com/mixpanel/android/util/JsonUtils.java +++ b/src/main/java/com/mixpanel/android/util/JsonUtils.java @@ -153,7 +153,23 @@ public static Map parseFlagsResponse(@Nullable JSON MPLog.w(LOGTAG, "Flag definition missing 'variant_value' for key: " + featureName + ". Assuming null value."); } - MixpanelFlagVariant flagData = new MixpanelFlagVariant(variantKey, variantValue); + // Parse optional experiment tracking fields + String experimentID = null; + if (flagDefinition.has(MPConstants.Flags.EXPERIMENT_ID) && !flagDefinition.isNull(MPConstants.Flags.EXPERIMENT_ID)) { + experimentID = flagDefinition.getString(MPConstants.Flags.EXPERIMENT_ID); + } + + Boolean isExperimentActive = null; + if (flagDefinition.has(MPConstants.Flags.IS_EXPERIMENT_ACTIVE) && !flagDefinition.isNull(MPConstants.Flags.IS_EXPERIMENT_ACTIVE)) { + isExperimentActive = flagDefinition.getBoolean(MPConstants.Flags.IS_EXPERIMENT_ACTIVE); + } + + Boolean isQATester = null; + if (flagDefinition.has(MPConstants.Flags.IS_QA_TESTER) && !flagDefinition.isNull(MPConstants.Flags.IS_QA_TESTER)) { + isQATester = flagDefinition.getBoolean(MPConstants.Flags.IS_QA_TESTER); + } + + MixpanelFlagVariant flagData = new MixpanelFlagVariant(variantKey, variantValue, experimentID, isExperimentActive, isQATester); flagsMap.put(featureName, flagData); } catch (JSONException e) { diff --git a/src/main/java/com/mixpanel/android/util/MPConstants.java b/src/main/java/com/mixpanel/android/util/MPConstants.java index dbe33646..576bed10 100644 --- a/src/main/java/com/mixpanel/android/util/MPConstants.java +++ b/src/main/java/com/mixpanel/android/util/MPConstants.java @@ -21,5 +21,8 @@ public static class Flags { public static final String FLAGS_KEY = "flags"; public static final String VARIANT_KEY = "variant_key"; public static final String VARIANT_VALUE = "variant_value"; + public static final String EXPERIMENT_ID = "experiment_id"; + public static final String IS_EXPERIMENT_ACTIVE = "is_experiment_active"; + public static final String IS_QA_TESTER = "is_qa_tester"; } }