Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@ private String createFlagsResponseJson(Map<String, MixpanelFlagVariant> 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();
Expand Down Expand Up @@ -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<String, MixpanelFlagVariant> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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;
}
}
18 changes: 17 additions & 1 deletion src/main/java/com/mixpanel/android/util/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,23 @@ public static Map<String, MixpanelFlagVariant> 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) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/mixpanel/android/util/MPConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Loading