From 0b39d102dfff2291fe2079d7b8b1117f975fc76a Mon Sep 17 00:00:00 2001 From: Mark Siebert <1504059+msiebert@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:41:53 +0000 Subject: [PATCH 1/3] add device id to feature flag context by default --- .../mpmetrics/FeatureFlagManagerTest.java | 35 +++++++++++++++---- .../mpmetrics/FeatureFlagDelegate.java | 1 + .../android/mpmetrics/FeatureFlagManager.java | 2 ++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java index a0603cef..eeb30c4e 100644 --- a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java +++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java @@ -74,6 +74,7 @@ public JSONObject getRequestBodyAsJson() throws JSONException { private static class MockFeatureFlagDelegate implements FeatureFlagDelegate { MPConfig configToReturn; String distinctIdToReturn = TEST_DISTINCT_ID; + String anonymousIdToReturn = "test_anonymous_id"; String tokenToReturn = TEST_TOKEN; List trackCalls = new ArrayList<>(); CountDownLatch trackCalledLatch; // Optional: for tests waiting for track @@ -101,6 +102,11 @@ public String getDistinctId() { return distinctIdToReturn; } + @Override + public String getAnonymousId() { + return anonymousIdToReturn; + } + @Override public void track(String eventName, JSONObject properties) { MPLog.v("FeatureFlagManagerTest", "MockDelegate.track called: " + eventName); @@ -940,6 +946,11 @@ public void testRequestBodyConstruction_performFetchRequest() throws Interrupted assertEquals("Context should contain correct distinct_id", "test_user_123", requestContext.getString("distinct_id")); + // Verify device_id is in the context + assertTrue("Context should contain device_id", requestContext.has("device_id")); + assertEquals("Context should contain correct device_id", + "test_anonymous_id", requestContext.getString("device_id")); + // Verify the context contains the expected properties from FlagsConfig assertEquals("Context should contain $os", "Android", requestContext.getString("$os")); assertEquals("Context should contain $os_version", "13", requestContext.getString("$os_version")); @@ -962,8 +973,9 @@ public void testRequestBodyConstruction_withNullContext() throws InterruptedExce // Setup with flags enabled but null context setupFlagsConfig(true, null); - // Set distinct ID + // Set distinct ID and anonymous ID mMockDelegate.distinctIdToReturn = "user_456"; + mMockDelegate.anonymousIdToReturn = "device_789"; // Create response Map serverFlags = new HashMap<>(); @@ -987,9 +999,13 @@ public void testRequestBodyConstruction_withNullContext() throws InterruptedExce // Verify distinct_id is in context assertEquals("Context should contain correct distinct_id", "user_456", requestContext.getString("distinct_id")); - // When FlagsConfig context is null, the context object should only contain distinct_id - assertEquals("Context should only contain distinct_id when FlagsConfig context is null", - 1, requestContext.length()); + // Verify device_id is in context + assertTrue("Context should contain device_id", requestContext.has("device_id")); + assertEquals("Context should contain correct device_id", "device_789", requestContext.getString("device_id")); + + // When FlagsConfig context is null, the context object should only contain distinct_id and device_id + assertEquals("Context should only contain distinct_id and device_id when FlagsConfig context is null", + 2, requestContext.length()); } @Test @@ -999,6 +1015,7 @@ public void testRequestBodyConstruction_withEmptyDistinctId() throws Interrupted // Set empty distinct ID mMockDelegate.distinctIdToReturn = ""; + mMockDelegate.anonymousIdToReturn = "device_empty_test"; // Create response Map serverFlags = new HashMap<>(); @@ -1022,6 +1039,10 @@ public void testRequestBodyConstruction_withEmptyDistinctId() throws Interrupted // Verify distinct_id is included in context even when empty assertTrue("Context should contain distinct_id field", requestContext.has("distinct_id")); assertEquals("Context should contain empty distinct_id", "", requestContext.getString("distinct_id")); + + // Verify device_id is included in context + assertTrue("Context should contain device_id field", requestContext.has("device_id")); + assertEquals("Context should contain correct device_id", "device_empty_test", requestContext.getString("device_id")); } @Test @@ -1114,9 +1135,11 @@ public void testFlagsConfigContextUsage_emptyContext() throws InterruptedExcepti JSONObject requestBody = capturedRequest.getRequestBodyAsJson(); JSONObject requestContext = requestBody.getJSONObject("context"); - // Context should only contain distinct_id when initial context is empty - assertEquals("Context should only contain distinct_id", 1, requestContext.length()); + // Context should only contain distinct_id and device_id when initial context is empty + assertEquals("Context should only contain distinct_id and device_id", 2, requestContext.length()); assertEquals("distinct_id should be present", "test_user", requestContext.getString("distinct_id")); + assertTrue("device_id should be present", requestContext.has("device_id")); + assertEquals("device_id should be present", "test_anonymous_id", requestContext.getString("device_id")); } @Test diff --git a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagDelegate.java b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagDelegate.java index d79a9752..1c190dc3 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagDelegate.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagDelegate.java @@ -9,6 +9,7 @@ interface FeatureFlagDelegate { MPConfig getMPConfig(); String getDistinctId(); + String getAnonymousId(); void track(String eventName, JSONObject properties); String getToken(); } \ No newline at end of file diff --git a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java index 33914411..6daccc9e 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java @@ -444,6 +444,7 @@ private void _performFetchRequest() { final MPConfig config = delegate.getMPConfig(); final String distinctId = delegate.getDistinctId(); + final String deviceId = delegate.getAnonymousId(); if (distinctId == null) { MPLog.w(LOGTAG, "Distinct ID is null. Cannot fetch flags."); @@ -456,6 +457,7 @@ private void _performFetchRequest() { // 1. Build Request Body JSON JSONObject contextJson = new JSONObject(mFlagsConfig.context.toString()); contextJson.put("distinct_id", distinctId); + contextJson.put("device_id", deviceId); JSONObject requestJson = new JSONObject(); requestJson.put("context", contextJson); String requestJsonString = requestJson.toString(); From d9101523888f237f0776de3243cf6b56db82e640 Mon Sep 17 00:00:00 2001 From: Mark Siebert <1504059+msiebert@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:45:31 +0000 Subject: [PATCH 2/3] handle null device id --- .../com/mixpanel/android/mpmetrics/FeatureFlagManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java index 6daccc9e..984fea5d 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java @@ -457,7 +457,9 @@ private void _performFetchRequest() { // 1. Build Request Body JSON JSONObject contextJson = new JSONObject(mFlagsConfig.context.toString()); contextJson.put("distinct_id", distinctId); - contextJson.put("device_id", deviceId); + if (deviceId != null) { + contextJson.put("device_id", deviceId); + } JSONObject requestJson = new JSONObject(); requestJson.put("context", contextJson); String requestJsonString = requestJson.toString(); From 1e74f3a4a9e9225317e0630e1eb2b456e77a7bdb Mon Sep 17 00:00:00 2001 From: Mark Siebert <1504059+msiebert@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:16:21 +0000 Subject: [PATCH 3/3] add timing tracking to feature flag calls --- .../android/mpmetrics/FeatureFlagManagerTest.java | 12 ++++++++++++ .../android/mpmetrics/FeatureFlagManager.java | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java index eeb30c4e..556c61d2 100644 --- a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java +++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java @@ -459,6 +459,18 @@ public void testTracking_getVariantSync_calledOnce() throws InterruptedException assertEquals("$experiment_started", call.eventName); assertEquals("track_flag_sync", call.properties.getString("Experiment name")); assertEquals("v_track_sync", call.properties.getString("Variant name")); + assertEquals("feature_flag", call.properties.getString("$experiment_type")); + + // Verify that timing properties are included + assertTrue("timeLastFetched should be present", call.properties.has("timeLastFetched")); + assertTrue("fetchLatencyMs should be present", call.properties.has("fetchLatencyMs")); + + // Verify that timing values are reasonable + long timeLastFetched = call.properties.getLong("timeLastFetched"); + long fetchLatencyMs = call.properties.getLong("fetchLatencyMs"); + assertTrue("timeLastFetched should be positive", timeLastFetched > 0); + assertTrue("fetchLatencyMs should be non-negative", fetchLatencyMs >= 0); + assertTrue("fetchLatencyMs should be reasonable (< 5 seconds)", fetchLatencyMs < 5000); } @Test diff --git a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java index 984fea5d..66b07879 100644 --- a/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java +++ b/src/main/java/com/mixpanel/android/mpmetrics/FeatureFlagManager.java @@ -49,6 +49,9 @@ class FeatureFlagManager implements MixpanelAPI.Flags { private final Set mTrackedFlags = new HashSet<>(); private boolean mIsFetching = false; private List> mFetchCompletionCallbacks = new ArrayList<>(); + // Track last fetch time and latency for $experiment_started event + private volatile long mTimeLastFetched = 0L; + private volatile long mFetchLatencyMs = 0L; // --- // Message codes for Handler @@ -430,6 +433,7 @@ private void _fetchFlagsIfNeeded(@Nullable FlagCompletionCallback compl * back to the mHandler thread via MSG_COMPLETE_FETCH. */ private void _performFetchRequest() { + long fetchStart = System.currentTimeMillis(); MPLog.v(LOGTAG, "Performing fetch request on thread: " + Thread.currentThread().getName()); boolean success = false; JSONObject responseJson = null; // To hold parsed successful response @@ -531,6 +535,9 @@ private void _performFetchRequest() { MPLog.e(LOGTAG, errorMessage, e); } + long fetchEnd = System.currentTimeMillis(); + mTimeLastFetched = fetchEnd; + mFetchLatencyMs = fetchEnd - fetchStart; // 6. Post result back to Handler thread postResultToHandler(success, responseJson, errorMessage); } @@ -629,6 +636,8 @@ private void _performTrackingDelegateCall(String flagName, MixpanelFlagVariant v properties.put("Experiment name", flagName); properties.put("Variant name", variant.key); // Use the variant key properties.put("$experiment_type", "feature_flag"); + properties.put("timeLastFetched", mTimeLastFetched); + properties.put("fetchLatencyMs", mFetchLatencyMs); } catch (JSONException e) { MPLog.e(LOGTAG, "Failed to create JSON properties for $experiment_started event", e); return; // Don't track if properties failed