diff --git a/acceptance/test-application/src/com/mixpanel/example/hello/MainActivity.java b/acceptance/test-application/src/com/mixpanel/example/hello/MainActivity.java
index 17cc74c79..64a73af9e 100644
--- a/acceptance/test-application/src/com/mixpanel/example/hello/MainActivity.java
+++ b/acceptance/test-application/src/com/mixpanel/example/hello/MainActivity.java
@@ -1,254 +1,240 @@
package com.mixpanel.example.hello;
import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Intent;
import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.BitmapDrawable;
-import android.net.Uri;
import android.os.Bundle;
import android.util.Base64;
-import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;
-
import com.mixpanel.android.mpmetrics.MixpanelAPI;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.FileNotFoundException;
-import java.io.InputStream;
import java.util.Calendar;
import java.util.Date;
+import org.json.JSONException;
+import org.json.JSONObject;
/**
* A little application that allows people to update their Mixpanel information.
*
- * For more information about integrating Mixpanel with your Android application,
- * please check out:
+ *
For more information about integrating Mixpanel with your Android application, please check
+ * out:
*
- * https://mixpanel.com/docs/integration-libraries/android
+ *
https://mixpanel.com/docs/integration-libraries/android
*
* @author mixpanel
- *
*/
public class MainActivity extends Activity {
- /*
- * You will use a Mixpanel API token to allow your app to send data to Mixpanel. To get your token
- * - Log in to Mixpanel, and select the project you want to use for this application
- * - Click the gear icon in the lower left corner of the screen to view the settings dialog
- * - In the settings dialog, you will see the label "Token", and a string that looks something like this:
- *
- * 2ef3c08e8466df98e67ea0cfa1512e9f
- *
- * Paste it below (where you see "YOUR API TOKEN")
- */
- public static final String MIXPANEL_API_TOKEN = "NOT A REAL TOKEN";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final String trackingDistinctId = getTrackingDistinctId();
-
- // Initialize the Mixpanel library for tracking.
- mMixpanel = MixpanelAPI.getInstance(this, MIXPANEL_API_TOKEN);
-
-
- // We also identify the current user with a distinct ID.
-
- mMixpanel.identify(trackingDistinctId); //this is the distinct_id value that
- // will be sent with events. If you choose not to set this,
- // the SDK will generate one for you
-
- mMixpanel.getPeople().identify(trackingDistinctId); //this is the distinct_id
- // that will be used for people analytics. You must set this explicitly in order
- // to dispatch people data.
-
- // People analytics must be identified separately from event analytics.
- // The data-sets are separate, and may have different unique keys (distinct_id).
- // We recommend using the same distinct_id value for a given user in both,
- // and identifying the user with that id as early as possible.
-
- setContentView(R.layout.activity_main);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.activity_main, menu);
- return true;
+ /*
+ * You will use a Mixpanel API token to allow your app to send data to Mixpanel. To get your token
+ * - Log in to Mixpanel, and select the project you want to use for this application
+ * - Click the gear icon in the lower left corner of the screen to view the settings dialog
+ * - In the settings dialog, you will see the label "Token", and a string that looks something like this:
+ *
+ * 2ef3c08e8466df98e67ea0cfa1512e9f
+ *
+ * Paste it below (where you see "YOUR API TOKEN")
+ */
+ public static final String MIXPANEL_API_TOKEN = "NOT A REAL TOKEN";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final String trackingDistinctId = getTrackingDistinctId();
+
+ // Initialize the Mixpanel library for tracking.
+ mMixpanel = MixpanelAPI.getInstance(this, MIXPANEL_API_TOKEN);
+
+ // We also identify the current user with a distinct ID.
+
+ mMixpanel.identify(trackingDistinctId); // this is the distinct_id value that
+ // will be sent with events. If you choose not to set this,
+ // the SDK will generate one for you
+
+ mMixpanel.getPeople().identify(trackingDistinctId); // this is the distinct_id
+ // that will be used for people analytics. You must set this explicitly in order
+ // to dispatch people data.
+
+ // People analytics must be identified separately from event analytics.
+ // The data-sets are separate, and may have different unique keys (distinct_id).
+ // We recommend using the same distinct_id value for a given user in both,
+ // and identifying the user with that id as early as possible.
+
+ setContentView(R.layout.activity_main);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.activity_main, menu);
+ return true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ final long nowInHours = hoursSinceEpoch();
+ final int hourOfTheDay = hourOfTheDay();
+
+ // For our simple test app, we're interested tracking
+ // when the user views our application.
+
+ // It will be interesting to segment our data by the date that they
+ // first viewed our app. We use a
+ // superProperty (so the value will always be sent with the
+ // remainder of our events) and register it with
+ // registerSuperPropertiesOnce (so no matter how many times
+ // the code below is run, the events will always be sent
+ // with the value of the first ever call for this user.)
+ // all the change we make below are LOCAL. No API requests are made.
+ try {
+ final JSONObject properties = new JSONObject();
+ properties.put("first viewed on", nowInHours);
+ properties.put("user domain", "(unknown)"); // default value
+ mMixpanel.registerSuperPropertiesOnce(properties);
+ } catch (final JSONException e) {
+ throw new RuntimeException("Could not encode hour first viewed as JSON");
}
- @Override
- protected void onResume() {
- super.onResume();
-
- final long nowInHours = hoursSinceEpoch();
- final int hourOfTheDay = hourOfTheDay();
-
- // For our simple test app, we're interested tracking
- // when the user views our application.
-
- // It will be interesting to segment our data by the date that they
- // first viewed our app. We use a
- // superProperty (so the value will always be sent with the
- // remainder of our events) and register it with
- // registerSuperPropertiesOnce (so no matter how many times
- // the code below is run, the events will always be sent
- // with the value of the first ever call for this user.)
- // all the change we make below are LOCAL. No API requests are made.
- try {
- final JSONObject properties = new JSONObject();
- properties.put("first viewed on", nowInHours);
- properties.put("user domain", "(unknown)"); // default value
- mMixpanel.registerSuperPropertiesOnce(properties);
- } catch (final JSONException e) {
- throw new RuntimeException("Could not encode hour first viewed as JSON");
- }
-
- // Now we send an event to Mixpanel. We want to send a new
- // "App Resumed" event every time we are resumed, and
- // we want to send a current value of "hour of the day" for every event.
- // As usual,all of the user's super properties will be appended onto this event.
- try {
- final JSONObject properties = new JSONObject();
- properties.put("hour of the day", hourOfTheDay);
- mMixpanel.track("App Resumed", properties);
- } catch(final JSONException e) {
- throw new RuntimeException("Could not encode hour of the day in JSON");
- }
- mMixpanel.getPeople().addOnMixpanelUpdatesReceivedListener(listener);
+ // Now we send an event to Mixpanel. We want to send a new
+ // "App Resumed" event every time we are resumed, and
+ // we want to send a current value of "hour of the day" for every event.
+ // As usual,all of the user's super properties will be appended onto this event.
+ try {
+ final JSONObject properties = new JSONObject();
+ properties.put("hour of the day", hourOfTheDay);
+ mMixpanel.track("App Resumed", properties);
+ } catch (final JSONException e) {
+ throw new RuntimeException("Could not encode hour of the day in JSON");
}
-
- // Associated with the "Send to Mixpanel" button in activity_main.xml
- // In this method, we update a Mixpanel people profile using MixpanelAPI.People.set()
- // and set some persistent properties that will be sent with
- // all future track() calls using MixpanelAPI.registerSuperProperties()
- public void sendToMixpanel(final View view) {
-
- final EditText firstNameEdit = (EditText) findViewById(R.id.edit_first_name);
- final EditText lastNameEdit = (EditText) findViewById(R.id.edit_last_name);
- final EditText emailEdit = (EditText) findViewById(R.id.edit_email_address);
-
- final String firstName = firstNameEdit.getText().toString();
- final String lastName = lastNameEdit.getText().toString();
- final String email = emailEdit.getText().toString();
-
- final MixpanelAPI.People people = mMixpanel.getPeople();
-
- // Update the basic data in the user's People Analytics record.
- // Unlike events, People Analytics always stores the most recent value
- // provided.
- people.set("$first_name", firstName);
- people.set("$last_name", lastName);
- people.set("$email", email);
-
- // We also want to keep track of how many times the user
- // has updated their info.
- people.increment("Update Count", 1L);
-
- // Mixpanel events are separate from Mixpanel people records,
- // but it might be valuable to be able to query events by
- // user domain (for example, if they represent customer organizations).
- //
- // We use the user domain as a superProperty here, but we call registerSuperProperties
- // instead of registerSuperPropertiesOnce so we can overwrite old values
- // as we get new information.
- try {
- final JSONObject domainProperty = new JSONObject();
- domainProperty.put("user domain", domainFromEmailAddress(email));
- mMixpanel.registerSuperProperties(domainProperty);
- } catch (final JSONException e) {
- throw new RuntimeException("Cannot write user email address domain as a super property");
- }
-
- // In addition to viewing the updated record in mixpanel's UI, it might
- // be interesting to see when and how many and what types of users
- // are updating their information, so we'll send an event as well.
- // You can call track with null if you don't have any properties to add
- // to an event (remember all the established superProperties will be added
- // before the event is dispatched to Mixpanel)
- mMixpanel.track("update info button clicked", null);
+ mMixpanel.getPeople().addOnMixpanelUpdatesReceivedListener(listener);
+ }
+
+ // Associated with the "Send to Mixpanel" button in activity_main.xml
+ // In this method, we update a Mixpanel people profile using MixpanelAPI.People.set()
+ // and set some persistent properties that will be sent with
+ // all future track() calls using MixpanelAPI.registerSuperProperties()
+ public void sendToMixpanel(final View view) {
+
+ final EditText firstNameEdit = (EditText) findViewById(R.id.edit_first_name);
+ final EditText lastNameEdit = (EditText) findViewById(R.id.edit_last_name);
+ final EditText emailEdit = (EditText) findViewById(R.id.edit_email_address);
+
+ final String firstName = firstNameEdit.getText().toString();
+ final String lastName = lastNameEdit.getText().toString();
+ final String email = emailEdit.getText().toString();
+
+ final MixpanelAPI.People people = mMixpanel.getPeople();
+
+ // Update the basic data in the user's People Analytics record.
+ // Unlike events, People Analytics always stores the most recent value
+ // provided.
+ people.set("$first_name", firstName);
+ people.set("$last_name", lastName);
+ people.set("$email", email);
+
+ // We also want to keep track of how many times the user
+ // has updated their info.
+ people.increment("Update Count", 1L);
+
+ // Mixpanel events are separate from Mixpanel people records,
+ // but it might be valuable to be able to query events by
+ // user domain (for example, if they represent customer organizations).
+ //
+ // We use the user domain as a superProperty here, but we call registerSuperProperties
+ // instead of registerSuperPropertiesOnce so we can overwrite old values
+ // as we get new information.
+ try {
+ final JSONObject domainProperty = new JSONObject();
+ domainProperty.put("user domain", domainFromEmailAddress(email));
+ mMixpanel.registerSuperProperties(domainProperty);
+ } catch (final JSONException e) {
+ throw new RuntimeException("Cannot write user email address domain as a super property");
}
- // This is an example of how you can use Mixpanel's revenue tracking features from Android.
- public void recordRevenue(final View view) {
- final MixpanelAPI.People people = mMixpanel.getPeople();
- // Call trackCharge() with a floating point amount
- // (for example, the amount of money the user has just spent on a purchase)
- // and an optional set of properties describing the purchase.
- people.trackCharge(1.50, null);
+ // In addition to viewing the updated record in mixpanel's UI, it might
+ // be interesting to see when and how many and what types of users
+ // are updating their information, so we'll send an event as well.
+ // You can call track with null if you don't have any properties to add
+ // to an event (remember all the established superProperties will be added
+ // before the event is dispatched to Mixpanel)
+ mMixpanel.track("update info button clicked", null);
+ }
+
+ // This is an example of how you can use Mixpanel's revenue tracking features from Android.
+ public void recordRevenue(final View view) {
+ final MixpanelAPI.People people = mMixpanel.getPeople();
+ // Call trackCharge() with a floating point amount
+ // (for example, the amount of money the user has just spent on a purchase)
+ // and an optional set of properties describing the purchase.
+ people.trackCharge(1.50, null);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // To preserve battery life, the Mixpanel library will store
+ // events rather than send them immediately. This means it
+ // is important to call flush() to send any unsent events
+ // before your application is taken out of memory.
+ mMixpanel.flush();
+ }
+
+ ////////////////////////////////////////////////////
+
+ private String getTrackingDistinctId() {
+ final SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+
+ String ret = prefs.getString(MIXPANEL_DISTINCT_ID_NAME, null);
+ if (ret == null) {
+ ret = generateDistinctId();
+ final SharedPreferences.Editor prefsEditor = prefs.edit();
+ prefsEditor.putString(MIXPANEL_DISTINCT_ID_NAME, ret);
+ prefsEditor.commit();
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- // To preserve battery life, the Mixpanel library will store
- // events rather than send them immediately. This means it
- // is important to call flush() to send any unsent events
- // before your application is taken out of memory.
- mMixpanel.flush();
+ return ret;
+ }
+
+ // These disinct ids are here for the purposes of illustration.
+ // In practice, there are great advantages to using distinct ids that
+ // are easily associated with user identity, either from server-side
+ // sources, or user logins. A common best practice is to maintain a field
+ // in your users table to store mixpanel distinct_id, so it is easily
+ // accesible for use in attributing cross platform or server side events.
+ private String generateDistinctId() {
+ final SecureRandom random = new SecureRandom();
+ final byte[] randomBytes = new byte[32];
+ random.nextBytes(randomBytes);
+ return Base64.encodeToString(randomBytes, Base64.NO_WRAP | Base64.NO_PADDING);
+ }
+
+ ///////////////////////////////////////////////////////
+ // conveniences
+
+ private int hourOfTheDay() {
+ final Calendar calendar = Calendar.getInstance();
+ return calendar.get(Calendar.HOUR_OF_DAY);
+ }
+
+ private long hoursSinceEpoch() {
+ final Date now = new Date();
+ final long nowMillis = now.getTime();
+ return nowMillis / 1000 * 60 * 60;
+ }
+
+ private String domainFromEmailAddress(String email) {
+ String ret = "";
+ final int atSymbolIndex = email.indexOf('@');
+ if ((atSymbolIndex > -1) && (email.length() > atSymbolIndex)) {
+ ret = email.substring(atSymbolIndex + 1);
}
- ////////////////////////////////////////////////////
-
- private String getTrackingDistinctId() {
- final SharedPreferences prefs = getPreferences(MODE_PRIVATE);
-
- String ret = prefs.getString(MIXPANEL_DISTINCT_ID_NAME, null);
- if (ret == null) {
- ret = generateDistinctId();
- final SharedPreferences.Editor prefsEditor = prefs.edit();
- prefsEditor.putString(MIXPANEL_DISTINCT_ID_NAME, ret);
- prefsEditor.commit();
- }
-
- return ret;
- }
-
- // These disinct ids are here for the purposes of illustration.
- // In practice, there are great advantages to using distinct ids that
- // are easily associated with user identity, either from server-side
- // sources, or user logins. A common best practice is to maintain a field
- // in your users table to store mixpanel distinct_id, so it is easily
- // accesible for use in attributing cross platform or server side events.
- private String generateDistinctId() {
- final SecureRandom random = new SecureRandom();
- final byte[] randomBytes = new byte[32];
- random.nextBytes(randomBytes);
- return Base64.encodeToString(randomBytes, Base64.NO_WRAP | Base64.NO_PADDING);
- }
-
- ///////////////////////////////////////////////////////
- // conveniences
-
- private int hourOfTheDay() {
- final Calendar calendar = Calendar.getInstance();
- return calendar.get(Calendar.HOUR_OF_DAY);
- }
-
- private long hoursSinceEpoch() {
- final Date now = new Date();
- final long nowMillis = now.getTime();
- return nowMillis / 1000 * 60 * 60;
- }
-
- private String domainFromEmailAddress(String email) {
- String ret = "";
- final int atSymbolIndex = email.indexOf('@');
- if ((atSymbolIndex > -1) && (email.length() > atSymbolIndex)) {
- ret = email.substring(atSymbolIndex + 1);
- }
-
- return ret;
- }
+ return ret;
+ }
- private MixpanelAPI mMixpanel;
- private static final String MIXPANEL_DISTINCT_ID_NAME = "Mixpanel Example $distinctid";
- private static final String LOGTAG = "Mixpanel Example Application";
+ private MixpanelAPI mMixpanel;
+ private static final String MIXPANEL_DISTINCT_ID_NAME = "Mixpanel Example $distinctid";
+ private static final String LOGTAG = "Mixpanel Example Application";
}
diff --git a/acceptance/test-application/src/com/mixpanel/example/hello/SessionManager.java b/acceptance/test-application/src/com/mixpanel/example/hello/SessionManager.java
index 0c8602d20..e1668bb8d 100644
--- a/acceptance/test-application/src/com/mixpanel/example/hello/SessionManager.java
+++ b/acceptance/test-application/src/com/mixpanel/example/hello/SessionManager.java
@@ -6,11 +6,6 @@
import android.os.Looper;
import android.os.Message;
import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -21,355 +16,349 @@
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
-
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
/**
- * This class serves as an example of how session tracking may be done on Android. The length of a session
- * is defined as the time between a call to startSession() and a call to endSession() after which there is
- * not another call to startSession() for at least 15 seconds. If a session has been started and
- * another startSession() function is called, it is a no op.
+ * This class serves as an example of how session tracking may be done on Android. The length of a
+ * session is defined as the time between a call to startSession() and a call to endSession() after
+ * which there is not another call to startSession() for at least 15 seconds. If a session has been
+ * started and another startSession() function is called, it is a no op.
*
- * This class is not officially supported by Mixpanel, and you may need to modify it for your own application.
+ *
This class is not officially supported by Mixpanel, and you may need to modify it for your own
+ * application.
*
- * Example Usage:
+ *
Example Usage:
*
- *
- * {@code
- * public class MainActivity extends ActionBarActivity {
- * @Override
- * protected void onCreate(Bundle savedInstanceState) {
- * super.onCreate(savedInstanceState);
- * setContentView(R.layout.activity_main);
+ * {@code
+ * public class MainActivity extends ActionBarActivity {
+ * @Override
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * setContentView(R.layout.activity_main);
*
- * this._sessionManager = SessionManager.getInstance(this, new SessionManager.SessionCompleteCallback() {
- * @Override
- * public void onSessionComplete(SessionManager.Session session) {
- * // You may send the session time to Mixpanel in here.
- * Log.d("MY APP", "session " + session.getUuid() + " lasted for " +
- * session.getSessionLength()/1000 + " seconds and is now closed");
- * }
- * });
- * this._sessionManager.startSession();
- * }
+ * this._sessionManager = SessionManager.getInstance(this, new SessionManager.SessionCompleteCallback() {
+ * @Override
+ * public void onSessionComplete(SessionManager.Session session) {
+ * // You may send the session time to Mixpanel in here.
+ * Log.d("MY APP", "session " + session.getUuid() + " lasted for " +
+ * session.getSessionLength()/1000 + " seconds and is now closed");
+ * }
+ * });
+ * this._sessionManager.startSession();
+ * }
*
- * @Override
- * public void onResume()
- * {
- * super.onResume();
- * this._sessionManager.startSession();
- * }
+ * @Override
+ * public void onResume()
+ * {
+ * super.onResume();
+ * this._sessionManager.startSession();
+ * }
*
- * @Override
- * public void onPause()
- * {
- * super.onPause();
- * this._sessionManager.endSession();
- * }
+ * @Override
+ * public void onPause()
+ * {
+ * super.onPause();
+ * this._sessionManager.endSession();
+ * }
*
- * private SessionManager _sessionManager;
- * }
+ * private SessionManager _sessionManager;
* }
- *
- *
+ * }
*/
public class SessionManager {
- /**
- * Instantiate a new SessionManager object
- * @param context
- * @param callback
- */
- private SessionManager(Context context, SessionCompleteCallback callback) {
- this._appContext = context.getApplicationContext();
- this._sessionCompleteCallback = callback; // this will be called any time a session is complete
- HandlerThread handlerThread = new HandlerThread(getClass().getCanonicalName());
- handlerThread.start();
- this._sessionHandler = new SessionHandler(this, handlerThread.getLooper());
- this._sessionHandler.sendEmptyMessage(MESSAGE_INIT);
+ /**
+ * Instantiate a new SessionManager object
+ *
+ * @param context
+ * @param callback
+ */
+ private SessionManager(Context context, SessionCompleteCallback callback) {
+ this._appContext = context.getApplicationContext();
+ this._sessionCompleteCallback = callback; // this will be called any time a session is complete
+ HandlerThread handlerThread = new HandlerThread(getClass().getCanonicalName());
+ handlerThread.start();
+ this._sessionHandler = new SessionHandler(this, handlerThread.getLooper());
+ this._sessionHandler.sendEmptyMessage(MESSAGE_INIT);
+ }
+
+ /**
+ * Get the SessionManager singleton object, create on if one doesn't exist
+ *
+ * @param context
+ * @param callback
+ * @return
+ */
+ public static SessionManager getInstance(Context context, SessionCompleteCallback callback) {
+ if (_instance == null) {
+ _instance = new SessionManager(context, callback);
}
-
- /**
- * Get the SessionManager singleton object, create on if one doesn't exist
- * @param context
- * @param callback
- * @return
- */
- public static SessionManager getInstance(Context context, SessionCompleteCallback callback) {
- if (_instance == null) {
- _instance = new SessionManager(context, callback);
+ return _instance;
+ }
+
+ /** Dispatch request to handler thread to start a session */
+ public void startSession() {
+ _sessionHandler.sendEmptyMessage(MESSAGE_START_SESSION);
+ }
+
+ /** Dispatch request to handler thread to end a session */
+ public void endSession() {
+ _sessionHandler.sendEmptyMessage(MESSAGE_END_SESSION);
+ }
+
+ /**
+ * Called by the handler thread, this will resume the previous session if it ended within the
+ * given threshold otherwise it'll create a new session. If a session already exists, it will be a
+ * noop.
+ */
+ private void _startSession() {
+ if (_curSession == null) {
+ if (_prevSession != null && !_prevSession.isExpired()) {
+ Log.d(LOGTAG, "resuming session " + _prevSession.getUuid());
+ _curSession = _prevSession;
+ _curSession.resume();
+ _prevSession = null;
+ } else {
+ _curSession = new Session();
+ Log.d(LOGTAG, "creating new session " + _curSession.getUuid());
+ synchronized (_sessionsLock) {
+ _sessions.add(_curSession);
+ _writeSessionsToFile();
+ this._initSessionCompleter();
}
- return _instance;
+ }
}
-
- /**
- * Dispatch request to handler thread to start a session
- */
- public void startSession() {
- _sessionHandler.sendEmptyMessage(MESSAGE_START_SESSION);
- }
-
-
- /**
- * Dispatch request to handler thread to end a session
- */
- public void endSession() {
- _sessionHandler.sendEmptyMessage(MESSAGE_END_SESSION);
+ }
+
+ /** Takes the current session, sets the end time, and sets it as the previous session. */
+ private void _endSession() {
+ if (_curSession != null) {
+ _curSession.end();
+ _prevSession = _curSession;
+ _curSession = null;
}
+ }
+
+ /**
+ * Spawns a thread to monitor for sessions that need to be completed (ended sessions that are
+ * guaranteed not to be resumed). If one is already running, this will be a noop.
+ */
+ private void _initSessionCompleter() {
+ if (_sessionCompleterThread == null || !_sessionCompleterThread.isAlive()) {
+ _sessionCompleterThread =
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ _completeExpiredSessions();
+ sleep(1000);
+ }
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "expiration watcher thread interrupted", e);
+ }
+ }
- /**
- * Called by the handler thread, this will resume the previous session if it ended within
- * the given threshold otherwise it'll create a new session. If a session already exists,
- * it will be a noop.
- */
- private void _startSession() {
- if (_curSession == null) {
- if (_prevSession != null && !_prevSession.isExpired()) {
- Log.d(LOGTAG, "resuming session " + _prevSession.getUuid());
- _curSession = _prevSession;
- _curSession.resume();
- _prevSession = null;
- } else {
- _curSession = new Session();
- Log.d(LOGTAG, "creating new session " + _curSession.getUuid());
- synchronized (_sessionsLock) {
- _sessions.add(_curSession);
+ private void _completeExpiredSessions() {
+ Log.d(LOGTAG, "checking for expired sessions...");
+ synchronized (_sessionsLock) {
+ Iterator iterator = _sessions.iterator();
+ while (iterator.hasNext()) {
+ Session session = iterator.next();
+ if (session.isExpired()) {
+ Log.d(LOGTAG, "expiring session id " + session.getUuid());
+ iterator.remove();
_writeSessionsToFile();
- this._initSessionCompleter();
+ _sessionCompleteCallback.onSessionComplete(session);
+ } else {
+ Log.d(LOGTAG, "session id " + session.getUuid() + " not yet expired...");
+ }
}
+ }
}
- }
+ };
+ _sessionCompleterThread.start();
}
-
- /**
- * Takes the current session, sets the end time, and sets it as the previous session.
- */
- private void _endSession() {
- if (_curSession != null) {
- _curSession.end();
- _prevSession = _curSession;
- _curSession = null;
+ }
+
+ /**
+ * Loads any previously non-completed sessions from local disk. This is necessary to guarantee
+ * that sessions are eventually completed when an app is hard-killed or crashes
+ */
+ private void _loadSessionsFromFile() {
+ FileInputStream fis = null;
+ try {
+ fis = _appContext.openFileInput(SESSIONS_FILE_NAME);
+ InputStreamReader isr = new InputStreamReader(fis);
+ BufferedReader bufferedReader = new BufferedReader(isr);
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line);
+ }
+ JSONArray sessionsJson = new JSONArray(sb.toString());
+
+ synchronized (_sessionsLock) {
+ for (int i = 0; i < sessionsJson.length(); i++) {
+ JSONObject sessionsObj = sessionsJson.getJSONObject(i);
+ Session session = new Session(sessionsObj);
+
+ // if there are sessions that don't have an end time we must assume that the
+ // app was killed mid session so we'll just send now as the end time. The better
+ // solution would be to periodically mark a "lastAccessTime" that can be used
+ // in such a case.
+ if (session.getEndTime() == null) {
+ session.end();
+ }
+
+ _sessions.add(session);
}
+ if (_sessions.size() > 0) {
+ this._initSessionCompleter();
+ }
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "Could not find sessions file", e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Could not read from sessions file", e);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Could not serialize json string from file", e);
+ }
+ }
+
+ /**
+ * Writes the current sessions list to local disk. This is so we have a persistent snapshot of
+ * non-completed sessions that can be reloaded in case of app shutdown / crash.
+ */
+ private void _writeSessionsToFile() {
+ FileOutputStream fos = null;
+ try {
+ fos = _appContext.openFileOutput(SESSIONS_FILE_NAME, Context.MODE_PRIVATE);
+ JSONArray jsonArray = new JSONArray();
+ for (Session session : _sessions) {
+ jsonArray.put(session.toJSON());
+ }
+ fos.write(jsonArray.toString().getBytes());
+ fos.close();
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "Could not find sessions file", e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Could not write to sessions file", e);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Could not turn session to JSON", e);
}
+ }
- /**
- * Spawns a thread to monitor for sessions that need to be completed (ended sessions that are
- * guaranteed not to be resumed). If one is already running, this will be a noop.
- */
- private void _initSessionCompleter() {
- if (_sessionCompleterThread == null || !_sessionCompleterThread.isAlive()) {
- _sessionCompleterThread = new Thread() {
- @Override
- public void run() {
- try {
- while (true) {
- _completeExpiredSessions();
- sleep(1000);
- }
- } catch (InterruptedException e) {
- Log.e(LOGTAG, "expiration watcher thread interrupted", e);
- }
- }
+ public class Session {
+ private String uuid = null;
+ private Long startTime = null;
+ private Long endTime = null;
+ private Long sessionExpirationGracePeriod = 15000L;
- private void _completeExpiredSessions() {
- Log.d(LOGTAG, "checking for expired sessions...");
- synchronized(_sessionsLock) {
- Iterator iterator = _sessions.iterator();
- while (iterator.hasNext()) {
- Session session = iterator.next();
- if (session.isExpired()) {
- Log.d(LOGTAG, "expiring session id " + session.getUuid());
- iterator.remove();
- _writeSessionsToFile();
- _sessionCompleteCallback.onSessionComplete(session);
- } else {
- Log.d(LOGTAG, "session id " + session.getUuid() + " not yet expired...");
- }
- }
-
- }
- }
- };
- _sessionCompleterThread.start();
- }
+ public Session() {
+ this.uuid = UUID.randomUUID().toString();
+ this.startTime = System.currentTimeMillis();
}
- /**
- * Loads any previously non-completed sessions from local disk. This is necessary to guarantee
- * that sessions are eventually completed when an app is hard-killed or crashes
- */
- private void _loadSessionsFromFile() {
- FileInputStream fis = null;
- try {
- fis = _appContext.openFileInput(SESSIONS_FILE_NAME);
- InputStreamReader isr = new InputStreamReader(fis);
- BufferedReader bufferedReader = new BufferedReader(isr);
- StringBuilder sb = new StringBuilder();
- String line;
- while ((line = bufferedReader.readLine()) != null) {
- sb.append(line);
- }
- JSONArray sessionsJson = new JSONArray(sb.toString());
-
- synchronized(_sessionsLock) {
- for (int i = 0; i < sessionsJson.length(); i++) {
- JSONObject sessionsObj = sessionsJson.getJSONObject(i);
- Session session = new Session(sessionsObj);
-
- // if there are sessions that don't have an end time we must assume that the
- // app was killed mid session so we'll just send now as the end time. The better
- // solution would be to periodically mark a "lastAccessTime" that can be used
- // in such a case.
- if (session.getEndTime() == null) {
- session.end();
- }
-
- _sessions.add(session);
- }
- if (_sessions.size() > 0) {
- this._initSessionCompleter();
- }
- }
- } catch (FileNotFoundException e) {
- Log.e(LOGTAG, "Could not find sessions file", e);
- } catch (IOException e) {
- Log.e(LOGTAG, "Could not read from sessions file", e);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Could not serialize json string from file", e);
- }
+ public Session(JSONObject jsonObject) throws JSONException {
+ this.uuid = jsonObject.getString("uuid");
+ this.startTime = jsonObject.getLong("startTime");
+ if (jsonObject.has("endTime")) {
+ this.endTime = jsonObject.getLong("endTime");
+ }
+ this.sessionExpirationGracePeriod = jsonObject.getLong("sessionExpirationGracePeriod");
}
- /**
- * Writes the current sessions list to local disk. This is so we have a persistent snapshot
- * of non-completed sessions that can be reloaded in case of app shutdown / crash.
- */
- private void _writeSessionsToFile() {
- FileOutputStream fos = null;
- try {
- fos = _appContext.openFileOutput(SESSIONS_FILE_NAME, Context.MODE_PRIVATE);
- JSONArray jsonArray = new JSONArray();
- for (Session session : _sessions) {
- jsonArray.put(session.toJSON());
- }
- fos.write(jsonArray.toString().getBytes());
- fos.close();
- } catch (FileNotFoundException e) {
- Log.e(LOGTAG, "Could not find sessions file", e);
- } catch (IOException e) {
- Log.e(LOGTAG, "Could not write to sessions file", e);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Could not turn session to JSON", e);
- }
+ public JSONObject toJSON() throws JSONException {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("uuid", this.uuid);
+ jsonObject.put("startTime", this.startTime);
+ jsonObject.put("endTime", this.endTime);
+ jsonObject.put("sessionExpirationGracePeriod", this.sessionExpirationGracePeriod);
+ return jsonObject;
}
- public class Session {
- private String uuid = null;
- private Long startTime = null;
- private Long endTime = null;
- private Long sessionExpirationGracePeriod = 15000L;
-
- public Session() {
- this.uuid = UUID.randomUUID().toString();
- this.startTime = System.currentTimeMillis();
- }
-
- public Session(JSONObject jsonObject) throws JSONException {
- this.uuid = jsonObject.getString("uuid");
- this.startTime = jsonObject.getLong("startTime");
- if (jsonObject.has("endTime")) {
- this.endTime = jsonObject.getLong("endTime");
- }
- this.sessionExpirationGracePeriod = jsonObject.getLong("sessionExpirationGracePeriod");
- }
-
- public JSONObject toJSON() throws JSONException {
- JSONObject jsonObject = new JSONObject();
- jsonObject.put("uuid", this.uuid);
- jsonObject.put("startTime", this.startTime);
- jsonObject.put("endTime", this.endTime);
- jsonObject.put("sessionExpirationGracePeriod", this.sessionExpirationGracePeriod);
- return jsonObject;
- }
-
- public void resume() {
- this.endTime = null;
- }
-
- public void end() {
- this.endTime = System.currentTimeMillis();
- }
+ public void resume() {
+ this.endTime = null;
+ }
- public boolean isExpired() {
- return this.endTime != null && System.currentTimeMillis() > this.endTime + this.sessionExpirationGracePeriod;
- }
+ public void end() {
+ this.endTime = System.currentTimeMillis();
+ }
- public Long getSessionLength() {
- if (this.endTime != null) {
- return this.endTime - this.startTime;
- } else {
- return System.currentTimeMillis() - this.startTime;
- }
- }
+ public boolean isExpired() {
+ return this.endTime != null
+ && System.currentTimeMillis() > this.endTime + this.sessionExpirationGracePeriod;
+ }
- public String getUuid() {
- return uuid;
- }
+ public Long getSessionLength() {
+ if (this.endTime != null) {
+ return this.endTime - this.startTime;
+ } else {
+ return System.currentTimeMillis() - this.startTime;
+ }
+ }
- public Long getStartTime() {
- return startTime;
- }
+ public String getUuid() {
+ return uuid;
+ }
- public Long getEndTime() {
- return endTime;
- }
+ public Long getStartTime() {
+ return startTime;
+ }
- public Long getSessionExpirationGracePeriod() {
- return sessionExpirationGracePeriod;
- }
+ public Long getEndTime() {
+ return endTime;
}
- public interface SessionCompleteCallback {
- public void onSessionComplete(Session session);
+ public Long getSessionExpirationGracePeriod() {
+ return sessionExpirationGracePeriod;
}
+ }
- /**
- * Handler thread responsible for all session interaction
- */
- public class SessionHandler extends Handler {
- private SessionManager sessionManager;
+ public interface SessionCompleteCallback {
+ public void onSessionComplete(Session session);
+ }
- public SessionHandler(SessionManager sessionManager, Looper looper) {
- super(looper);
- this.sessionManager = sessionManager;
- }
+ /** Handler thread responsible for all session interaction */
+ public class SessionHandler extends Handler {
+ private SessionManager sessionManager;
- @Override
- public void handleMessage(Message msg) {
- switch(msg.what) {
- case MESSAGE_INIT:
- sessionManager._loadSessionsFromFile();
- break;
- case MESSAGE_START_SESSION:
- sessionManager._startSession();
- break;
- case MESSAGE_END_SESSION:
- sessionManager._endSession();
- break;
- }
- }
+ public SessionHandler(SessionManager sessionManager, Looper looper) {
+ super(looper);
+ this.sessionManager = sessionManager;
}
- private static String LOGTAG = "SessionManager";
- private static String SESSIONS_FILE_NAME = "user_sessions";
- private static final int MESSAGE_INIT = 0;
- private static final int MESSAGE_START_SESSION = 1;
- private static final int MESSAGE_END_SESSION = 2;
-
- private static SessionManager _instance = null;
- private static final Object[] _sessionsLock = new Object[0];
- private List _sessions = new ArrayList();
- private Session _curSession = null;
- private Session _prevSession = null;
- private SessionHandler _sessionHandler;
- private Context _appContext = null;
- private Thread _sessionCompleterThread = null;
- private final SessionCompleteCallback _sessionCompleteCallback;
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_INIT:
+ sessionManager._loadSessionsFromFile();
+ break;
+ case MESSAGE_START_SESSION:
+ sessionManager._startSession();
+ break;
+ case MESSAGE_END_SESSION:
+ sessionManager._endSession();
+ break;
+ }
+ }
+ }
+
+ private static String LOGTAG = "SessionManager";
+ private static String SESSIONS_FILE_NAME = "user_sessions";
+ private static final int MESSAGE_INIT = 0;
+ private static final int MESSAGE_START_SESSION = 1;
+ private static final int MESSAGE_END_SESSION = 2;
+
+ private static SessionManager _instance = null;
+ private static final Object[] _sessionsLock = new Object[0];
+ private List _sessions = new ArrayList();
+ private Session _curSession = null;
+ private Session _prevSession = null;
+ private SessionHandler _sessionHandler;
+ private Context _appContext = null;
+ private Thread _sessionCompleterThread = null;
+ private final SessionCompleteCallback _sessionCompleteCallback;
}
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/AutomaticEventsTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/AutomaticEventsTest.java
index 85ac728f2..beab22cd6 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/AutomaticEventsTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/AutomaticEventsTest.java
@@ -1,330 +1,394 @@
package com.mixpanel.android.mpmetrics;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
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;
-
import com.mixpanel.android.util.Base64Coder;
import com.mixpanel.android.util.HttpService;
import com.mixpanel.android.util.ProxyServerInteractor;
import com.mixpanel.android.util.RemoteService;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
-
import javax.net.ssl.SSLSocketFactory;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
-/**
- * Created by sergioalonso on 5/16/17.
- */
-
+/** Created by sergioalonso on 5/16/17. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class AutomaticEventsTest {
- private MixpanelAPI mCleanMixpanelAPI;
- private static final String TOKEN = "Automatic Events Token";
- private static final int MAX_TIMEOUT_POLL = 6500;
- final private BlockingQueue mPerformRequestEvents = new LinkedBlockingQueue<>();
- private Future mMockReferrerPreferences;
- private int mTrackedEvents;
- private CountDownLatch mLatch = new CountDownLatch(1);
- private MPDbAdapter mockAdapter;
- private CountDownLatch mMinRequestsLatch;
-
- @Before
- public void setUp() {
- mMockReferrerPreferences = new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
- mTrackedEvents = 0;
- mMinRequestsLatch = new CountDownLatch(2); // First Time Open and Update
- }
-
- private void setUpInstance(boolean trackAutomaticEvents) {
- final RemoteService mockPoster = new HttpService() {
- @Override
- public byte[] performRequest(
- @NonNull String endpointUrl,
- @Nullable ProxyServerInteractor interactor,
- @Nullable Map params, // Used only if requestBodyBytes is null
- @Nullable Map 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 {
- JSONArray jsonArray = new JSONArray(jsonData);
- for (int i = 0; i < jsonArray.length(); i++) {
- mPerformRequestEvents.put(jsonArray.getJSONObject(i).getString("event"));
- mMinRequestsLatch.countDown();
- }
- return TestUtils.bytes("1\n");
- } catch (JSONException e) {
- throw new RuntimeException("Malformed data passed to test mock", e);
- } catch (InterruptedException e) {
- throw new RuntimeException("Could not write message to reporting queue for tests.", e);
- }
- }
- };
-
- InstrumentationRegistry.getInstrumentation().getContext().deleteDatabase("mixpanel");
- mockAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void cleanupEvents(String last_id, Table table, String token) {
- if (token.equalsIgnoreCase(TOKEN)) {
- super.cleanupEvents(last_id, table, token);
- }
- }
-
- @Override
- public int addJSON(JSONObject j, String token, Table table) {
- if (token.equalsIgnoreCase(TOKEN)) {
- mTrackedEvents++;
- mLatch.countDown();
- return super.addJSON(j, token, table);
- }
-
- return 1;
+ private MixpanelAPI mCleanMixpanelAPI;
+ private static final String TOKEN = "Automatic Events Token";
+ private static final int MAX_TIMEOUT_POLL = 6500;
+ private final BlockingQueue mPerformRequestEvents = new LinkedBlockingQueue<>();
+ private Future mMockReferrerPreferences;
+ private int mTrackedEvents;
+ private CountDownLatch mLatch = new CountDownLatch(1);
+ private MPDbAdapter mockAdapter;
+ private CountDownLatch mMinRequestsLatch;
+
+ @Before
+ public void setUp() {
+ mMockReferrerPreferences =
+ new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
+ mTrackedEvents = 0;
+ mMinRequestsLatch = new CountDownLatch(2); // First Time Open and Update
+ }
+
+ private void setUpInstance(boolean trackAutomaticEvents) {
+ final RemoteService mockPoster =
+ new HttpService() {
+ @Override
+ public byte[] performRequest(
+ @NonNull String endpointUrl,
+ @Nullable ProxyServerInteractor interactor,
+ @Nullable Map params, // Used only if requestBodyBytes is null
+ @Nullable Map 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 {
+ JSONArray jsonArray = new JSONArray(jsonData);
+ for (int i = 0; i < jsonArray.length(); i++) {
+ mPerformRequestEvents.put(jsonArray.getJSONObject(i).getString("event"));
+ mMinRequestsLatch.countDown();
+ }
+ return TestUtils.bytes("1\n");
+ } catch (JSONException e) {
+ throw new RuntimeException("Malformed data passed to test mock", e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(
+ "Could not write message to reporting queue for tests.", e);
}
+ }
};
- final AnalyticsMessages automaticAnalyticsMessages = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
-
- @Override
- protected RemoteService getPoster() {
- return mockPoster;
+ InstrumentationRegistry.getInstrumentation().getContext().deleteDatabase("mixpanel");
+ mockAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void cleanupEvents(String last_id, Table table, String token) {
+ if (token.equalsIgnoreCase(TOKEN)) {
+ super.cleanupEvents(last_id, table, token);
}
-
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mockAdapter;
+ }
+
+ @Override
+ public int addJSON(JSONObject j, String token, Table table) {
+ if (token.equalsIgnoreCase(TOKEN)) {
+ mTrackedEvents++;
+ mLatch.countDown();
+ return super.addJSON(j, token, table);
}
- @Override
- protected Worker createWorker() {
- return new Worker() {
- @Override
- protected Handler restartWorkerThread() {
- final HandlerThread thread = new HandlerThread("com.mixpanel.android.AnalyticsWorker", Process.THREAD_PRIORITY_BACKGROUND);
- thread.start();
- final Handler ret = new AnalyticsMessageHandler(thread.getLooper()) {
- };
- return ret;
- }
- };
- }
+ return 1;
+ }
};
- mCleanMixpanelAPI = new MixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockReferrerPreferences, TOKEN, false, null, trackAutomaticEvents) {
-
- @Override
- /* package */ PersistentIdentity getPersistentIdentity(final Context context, final Future referrerPreferences, final String token, final String instanceName) {
- String instanceKey = instanceName != null ? instanceName : token;
- final String prefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI_" + instanceKey;
- final SharedPreferences ret = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
- ret.edit().clear().commit();
-
- final String timeEventsPrefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI.TimeEvents_" + instanceKey;
- final SharedPreferences timeSharedPrefs = context.getSharedPreferences(timeEventsPrefsName, Context.MODE_PRIVATE);
- timeSharedPrefs.edit().clear().commit();
-
- final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
- final SharedPreferences mpSharedPrefs = context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
- mpSharedPrefs.edit().clear().putInt("latest_version_code", -2).commit(); // -1 is the default value
-
- return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
- }
-
- @Override
- AnalyticsMessages getAnalyticsMessages() {
- return automaticAnalyticsMessages;
- }
+ final AnalyticsMessages automaticAnalyticsMessages =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+
+ @Override
+ protected RemoteService getPoster() {
+ return mockPoster;
+ }
+
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mockAdapter;
+ }
+
+ @Override
+ protected Worker createWorker() {
+ return new Worker() {
+ @Override
+ protected Handler restartWorkerThread() {
+ final HandlerThread thread =
+ new HandlerThread(
+ "com.mixpanel.android.AnalyticsWorker", Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ final Handler ret = new AnalyticsMessageHandler(thread.getLooper()) {};
+ return ret;
+ }
+ };
+ }
};
- }
-
- @After
- public void tearDown() throws Exception {
- mMinRequestsLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS);
- }
- @Test
- public void testAutomaticOneInstance() throws InterruptedException {
- int calls = 3; // First Time Open, App Update, An Event One
- mLatch = new CountDownLatch(calls);
- setUpInstance(true);
- mCleanMixpanelAPI.track("An event One");
- mCleanMixpanelAPI.flush();
- assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(calls, mTrackedEvents);
- assertEquals(AutomaticEvents.FIRST_OPEN, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(AutomaticEvents.APP_UPDATED, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals("An event One", mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void testDisableAutomaticEvents() throws InterruptedException {
- int calls = 1;
- setUpInstance(false);
- mLatch = new CountDownLatch(calls);
- mCleanMixpanelAPI.track("An Event Three");
- assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(calls, mTrackedEvents);
-
- mCleanMixpanelAPI.track("Automatic Event Two", null, true); // dropped
- mCleanMixpanelAPI.track("Automatic Event Three", null, true); // dropped
- mCleanMixpanelAPI.track("Automatic Event Four", null, true); // dropped
- mCleanMixpanelAPI.flush();
- assertEquals("An Event Three", mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
-
- String[] noEvents = mockAdapter.generateDataString(MPDbAdapter.Table.EVENTS, TOKEN);
- assertNull(noEvents);
-
- mCleanMixpanelAPI.flush();
- assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void testAutomaticMultipleInstances() throws InterruptedException {
- final String SECOND_TOKEN = "Automatic Events Token Two";
- int initialCalls = 2;
- setUpInstance(true);
- mLatch = new CountDownLatch(initialCalls);
-
- final CountDownLatch secondLatch = new CountDownLatch(initialCalls);
- final BlockingQueue secondPerformedRequests = new LinkedBlockingQueue<>();
-
- final HttpService mpSecondPoster = new HttpService() {
- @Override
- public byte[] performRequest(
- @NonNull String endpointUrl,
- @Nullable ProxyServerInteractor interactor,
- @Nullable Map params, // Used only if requestBodyBytes is null
- @Nullable Map 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 {
- JSONArray jsonArray = new JSONArray(jsonData);
- for (int i = 0; i < jsonArray.length(); i++) {
- secondPerformedRequests.put(jsonArray.getJSONObject(i).getString("event"));
- }
- return TestUtils.bytes("1\n");
- } catch (JSONException e) {
- throw new RuntimeException("Malformed data passed to test mock", e);
- } catch (InterruptedException e) {
- throw new RuntimeException("Could not write message to reporting queue for tests.", e);
- }
- }
+ mCleanMixpanelAPI =
+ new MixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockReferrerPreferences,
+ TOKEN,
+ false,
+ null,
+ trackAutomaticEvents) {
+
+ @Override
+ /* package */ PersistentIdentity getPersistentIdentity(
+ final Context context,
+ final Future referrerPreferences,
+ final String token,
+ final String instanceName) {
+ String instanceKey = instanceName != null ? instanceName : token;
+ final String prefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI_" + instanceKey;
+ final SharedPreferences ret =
+ context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
+ ret.edit().clear().commit();
+
+ final String timeEventsPrefsName =
+ "com.mixpanel.android.mpmetrics.MixpanelAPI.TimeEvents_" + instanceKey;
+ final SharedPreferences timeSharedPrefs =
+ context.getSharedPreferences(timeEventsPrefsName, Context.MODE_PRIVATE);
+ timeSharedPrefs.edit().clear().commit();
+
+ final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
+ final SharedPreferences mpSharedPrefs =
+ context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
+ mpSharedPrefs
+ .edit()
+ .clear()
+ .putInt("latest_version_code", -2)
+ .commit(); // -1 is the default value
+
+ return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
+ }
+
+ @Override
+ AnalyticsMessages getAnalyticsMessages() {
+ return automaticAnalyticsMessages;
+ }
};
-
- final MPDbAdapter mpSecondDbAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void cleanupEvents(String last_id, Table table, String token) {
- if (token.equalsIgnoreCase(SECOND_TOKEN)) {
- super.cleanupEvents(last_id, table, token);
- }
- }
-
- @Override
- public int addJSON(JSONObject j, String token, Table table) {
- if (token.equalsIgnoreCase(SECOND_TOKEN)) {
- secondLatch.countDown();
- return super.addJSON(j, token, table);
- }
-
- return 1;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mMinRequestsLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testAutomaticOneInstance() throws InterruptedException {
+ int calls = 3; // First Time Open, App Update, An Event One
+ mLatch = new CountDownLatch(calls);
+ setUpInstance(true);
+ mCleanMixpanelAPI.track("An event One");
+ mCleanMixpanelAPI.flush();
+ assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(calls, mTrackedEvents);
+ assertEquals(
+ AutomaticEvents.FIRST_OPEN,
+ mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(
+ AutomaticEvents.APP_UPDATED,
+ mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(
+ "An event One", mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testDisableAutomaticEvents() throws InterruptedException {
+ int calls = 1;
+ setUpInstance(false);
+ mLatch = new CountDownLatch(calls);
+ mCleanMixpanelAPI.track("An Event Three");
+ assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(calls, mTrackedEvents);
+
+ mCleanMixpanelAPI.track("Automatic Event Two", null, true); // dropped
+ mCleanMixpanelAPI.track("Automatic Event Three", null, true); // dropped
+ mCleanMixpanelAPI.track("Automatic Event Four", null, true); // dropped
+ mCleanMixpanelAPI.flush();
+ assertEquals(
+ "An Event Three", mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+
+ String[] noEvents = mockAdapter.generateDataString(MPDbAdapter.Table.EVENTS, TOKEN);
+ assertNull(noEvents);
+
+ mCleanMixpanelAPI.flush();
+ assertEquals(null, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testAutomaticMultipleInstances() throws InterruptedException {
+ final String SECOND_TOKEN = "Automatic Events Token Two";
+ int initialCalls = 2;
+ setUpInstance(true);
+ mLatch = new CountDownLatch(initialCalls);
+
+ final CountDownLatch secondLatch = new CountDownLatch(initialCalls);
+ final BlockingQueue secondPerformedRequests = new LinkedBlockingQueue<>();
+
+ final HttpService mpSecondPoster =
+ new HttpService() {
+ @Override
+ public byte[] performRequest(
+ @NonNull String endpointUrl,
+ @Nullable ProxyServerInteractor interactor,
+ @Nullable Map params, // Used only if requestBodyBytes is null
+ @Nullable Map 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 {
+ JSONArray jsonArray = new JSONArray(jsonData);
+ for (int i = 0; i < jsonArray.length(); i++) {
+ secondPerformedRequests.put(jsonArray.getJSONObject(i).getString("event"));
+ }
+ return TestUtils.bytes("1\n");
+ } catch (JSONException e) {
+ throw new RuntimeException("Malformed data passed to test mock", e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(
+ "Could not write message to reporting queue for tests.", e);
}
+ }
};
- final AnalyticsMessages mpSecondAnalyticsMessages = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- protected RemoteService getPoster() {
- return mpSecondPoster;
+ final MPDbAdapter mpSecondDbAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void cleanupEvents(String last_id, Table table, String token) {
+ if (token.equalsIgnoreCase(SECOND_TOKEN)) {
+ super.cleanupEvents(last_id, table, token);
}
+ }
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mpSecondDbAdapter;
+ @Override
+ public int addJSON(JSONObject j, String token, Table table) {
+ if (token.equalsIgnoreCase(SECOND_TOKEN)) {
+ secondLatch.countDown();
+ return super.addJSON(j, token, table);
}
- @Override
- protected Worker createWorker() {
- return new Worker() {
- @Override
- protected Handler restartWorkerThread() {
- final HandlerThread thread = new HandlerThread("com.mixpanel.android.AnalyticsWorker", Process.THREAD_PRIORITY_BACKGROUND);
- thread.start();
- final Handler ret = new AnalyticsMessageHandler(thread.getLooper()) {
- };
- return ret;
- }
- };
- }
+ return 1;
+ }
};
- MixpanelAPI mpSecondInstance = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext()), SECOND_TOKEN, false) {
- @Override
- AnalyticsMessages getAnalyticsMessages() {
- return mpSecondAnalyticsMessages;
- }
+ final AnalyticsMessages mpSecondAnalyticsMessages =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ protected RemoteService getPoster() {
+ return mpSecondPoster;
+ }
+
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mpSecondDbAdapter;
+ }
+
+ @Override
+ protected Worker createWorker() {
+ return new Worker() {
+ @Override
+ protected Handler restartWorkerThread() {
+ final HandlerThread thread =
+ new HandlerThread(
+ "com.mixpanel.android.AnalyticsWorker", Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ final Handler ret = new AnalyticsMessageHandler(thread.getLooper()) {};
+ return ret;
+ }
+ };
+ }
+ };
+
+ MixpanelAPI mpSecondInstance =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ new TestUtils.EmptyPreferences(
+ InstrumentationRegistry.getInstrumentation().getContext()),
+ SECOND_TOKEN,
+ false) {
+ @Override
+ AnalyticsMessages getAnalyticsMessages() {
+ return mpSecondAnalyticsMessages;
+ }
};
- assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(initialCalls, mTrackedEvents);
- mLatch = new CountDownLatch(MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null).getBulkUploadLimit() - initialCalls);
- for (int i = 0; i < MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null).getBulkUploadLimit() - initialCalls; i++) {
- mCleanMixpanelAPI.track("Track event " + i);
- }
-
- assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- mCleanMixpanelAPI.flush();
-
- assertEquals(AutomaticEvents.FIRST_OPEN, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals(AutomaticEvents.APP_UPDATED, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- for (int i = 0; i < MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null).getBulkUploadLimit() - initialCalls; i++) {
- assertEquals("Track event " + i, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- }
-
- assertNull(mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertNull(secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
-
- mpSecondInstance.flush();
- mCleanMixpanelAPI.track("First Instance Event One");
- mpSecondInstance.track("Second Instance Event One");
- mpSecondInstance.track("Second Instance Event Two");
- mpSecondInstance.flush();
-
- assertEquals("Second Instance Event One", secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertEquals("Second Instance Event Two", secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
- assertNull(secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
-
- assertNull(mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(initialCalls, mTrackedEvents);
+ mLatch =
+ new CountDownLatch(
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)
+ .getBulkUploadLimit()
+ - initialCalls);
+ for (int i = 0;
+ i
+ < MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)
+ .getBulkUploadLimit()
+ - initialCalls;
+ i++) {
+ mCleanMixpanelAPI.track("Track event " + i);
+ }
+
+ assertTrue(mLatch.await(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ mCleanMixpanelAPI.flush();
+
+ assertEquals(
+ AutomaticEvents.FIRST_OPEN,
+ mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(
+ AutomaticEvents.APP_UPDATED,
+ mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ for (int i = 0;
+ i
+ < MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)
+ .getBulkUploadLimit()
+ - initialCalls;
+ i++) {
+ assertEquals(
+ "Track event " + i, mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
}
+
+ assertNull(mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertNull(secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+
+ mpSecondInstance.flush();
+ mCleanMixpanelAPI.track("First Instance Event One");
+ mpSecondInstance.track("Second Instance Event One");
+ mpSecondInstance.track("Second Instance Event Two");
+ mpSecondInstance.flush();
+
+ assertEquals(
+ "Second Instance Event One",
+ secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertEquals(
+ "Second Instance Event Two",
+ secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ assertNull(secondPerformedRequests.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+
+ assertNull(mPerformRequestEvents.poll(MAX_TIMEOUT_POLL, TimeUnit.MILLISECONDS));
+ }
}
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java
index a0603cef3..ec56487f0 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/FeatureFlagManagerTest.java
@@ -1,27 +1,19 @@
package com.mixpanel.android.mpmetrics;
+import static org.junit.Assert.*;
+
import android.content.Context;
import android.os.Bundle;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-
import com.mixpanel.android.util.MPLog;
import com.mixpanel.android.util.OfflineMode; // Assuming this exists
import com.mixpanel.android.util.ProxyServerInteractor;
import com.mixpanel.android.util.RemoteService;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import java.io.IOException;
+import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
@@ -33,1166 +25,1316 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
-import java.lang.reflect.Field;
-
import javax.net.ssl.SSLSocketFactory;
-
-import static org.junit.Assert.*;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class FeatureFlagManagerTest {
- private FeatureFlagManager mFeatureFlagManager;
- private MockFeatureFlagDelegate mMockDelegate;
- private MockRemoteService mMockRemoteService;
- private MPConfig mTestConfig;
- private Context mContext;
-
- private static final String TEST_SERVER_URL = "https://test.mixpanel.com";
- private static final String TEST_DISTINCT_ID = "test_distinct_id";
- private static final String TEST_TOKEN = "test_token";
- private static final long ASYNC_TEST_TIMEOUT_MS = 2000; // 2 seconds
-
- // Helper class for capturing requests made to the mock service
- private static class CapturedRequest {
- final String endpointUrl;
- final Map headers;
- final byte[] requestBodyBytes;
-
- CapturedRequest(String endpointUrl, Map headers, byte[] requestBodyBytes) {
- this.endpointUrl = endpointUrl;
- this.headers = headers;
- this.requestBodyBytes = requestBodyBytes;
- }
-
- public JSONObject getRequestBodyAsJson() throws JSONException {
- if (requestBodyBytes == null) return null;
- return new JSONObject(new String(requestBodyBytes, StandardCharsets.UTF_8));
- }
- }
-
- private static class MockFeatureFlagDelegate implements FeatureFlagDelegate {
- MPConfig configToReturn;
- String distinctIdToReturn = TEST_DISTINCT_ID;
- String tokenToReturn = TEST_TOKEN;
- List trackCalls = new ArrayList<>();
- CountDownLatch trackCalledLatch; // Optional: for tests waiting for track
-
- static class TrackCall {
- final String eventName;
- final JSONObject properties;
- TrackCall(String eventName, JSONObject properties) {
- this.eventName = eventName;
- this.properties = properties;
- }
- }
-
- public MockFeatureFlagDelegate(MPConfig config) {
- this.configToReturn = config;
- }
-
- @Override
- public MPConfig getMPConfig() {
- return configToReturn;
- }
-
- @Override
- public String getDistinctId() {
- return distinctIdToReturn;
- }
-
- @Override
- public void track(String eventName, JSONObject properties) {
- MPLog.v("FeatureFlagManagerTest", "MockDelegate.track called: " + eventName);
- trackCalls.add(new TrackCall(eventName, properties));
- if (trackCalledLatch != null) {
- trackCalledLatch.countDown();
- }
- }
-
- @Override
- public String getToken() {
- return tokenToReturn;
- }
-
- public void resetTrackCalls() {
- trackCalls.clear();
- trackCalledLatch = null;
- }
- }
-
- private static class MockRemoteService implements RemoteService {
- // Queue to hold responses/exceptions to be returned by performRequest
- private final BlockingQueue mResults = new ArrayBlockingQueue<>(10);
- // Queue to capture actual requests made
- private final BlockingQueue mCapturedRequests = new ArrayBlockingQueue<>(10);
-
- @Override
- public boolean isOnline(Context context, OfflineMode offlineMode) {
- return true; // Assume online for tests unless specified
- }
-
- @Override
- public void checkIsMixpanelBlocked() {
- // No-op for tests
- }
-
- @Override
- public byte[] performRequest(
- @NonNull String endpointUrl,
- @Nullable ProxyServerInteractor interactor,
- @Nullable Map params,
- @Nullable Map headers,
- @Nullable byte[] requestBodyBytes,
- @Nullable SSLSocketFactory socketFactory)
- throws ServiceUnavailableException, IOException {
-
- mCapturedRequests.offer(new CapturedRequest(endpointUrl, headers, requestBodyBytes));
-
- try {
- Object result = mResults.poll(FeatureFlagManagerTest.ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- if (result == null) {
- throw new IOException("MockRemoteService timed out waiting for a result to be queued.");
- }
- if (result instanceof IOException) {
- throw (IOException) result;
- }
- if (result instanceof ServiceUnavailableException) {
- throw (ServiceUnavailableException) result;
- }
- if (result instanceof RuntimeException) { // For other test exceptions
- throw (RuntimeException) result;
- }
- return (byte[]) result;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("MockRemoteService interrupted.", e);
- }
- }
-
- public void addResponse(byte[] responseBytes) {
- mResults.offer(responseBytes);
- }
-
- public void addResponse(JSONObject responseJson) {
- mResults.offer(responseJson.toString().getBytes(StandardCharsets.UTF_8));
- }
-
- public void addError(Exception e) {
- mResults.offer(e);
- }
-
- public CapturedRequest takeRequest(long timeout, TimeUnit unit) throws InterruptedException {
- return mCapturedRequests.poll(timeout, unit);
- }
-
- public void reset() {
- mResults.clear();
- mCapturedRequests.clear();
- }
- }
-
- @Before
- public void setUp() {
- mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
- // MPConfig requires a context and a token (even if we override methods)
- // Create a basic MPConfig. Specific flag settings will be set via MockDelegate.
- mTestConfig = new MPConfig(new Bundle(), mContext, TEST_TOKEN);
-
-
- mMockDelegate = new MockFeatureFlagDelegate(mTestConfig);
- mMockRemoteService = new MockRemoteService();
-
- mFeatureFlagManager = new FeatureFlagManager(
- mMockDelegate, // Pass delegate directly, manager will wrap in WeakReference
- mMockRemoteService,
- new FlagsConfig(true, new JSONObject())
- );
- MPLog.setLevel(MPLog.VERBOSE); // Enable verbose logging for tests
+ private FeatureFlagManager mFeatureFlagManager;
+ private MockFeatureFlagDelegate mMockDelegate;
+ private MockRemoteService mMockRemoteService;
+ private MPConfig mTestConfig;
+ private Context mContext;
+
+ private static final String TEST_SERVER_URL = "https://test.mixpanel.com";
+ private static final String TEST_DISTINCT_ID = "test_distinct_id";
+ private static final String TEST_TOKEN = "test_token";
+ private static final long ASYNC_TEST_TIMEOUT_MS = 2000; // 2 seconds
+
+ // Helper class for capturing requests made to the mock service
+ private static class CapturedRequest {
+ final String endpointUrl;
+ final Map headers;
+ final byte[] requestBodyBytes;
+
+ CapturedRequest(String endpointUrl, Map headers, byte[] requestBodyBytes) {
+ this.endpointUrl = endpointUrl;
+ this.headers = headers;
+ this.requestBodyBytes = requestBodyBytes;
}
- @After
- public void tearDown() {
- // Ensure handler thread is quit if it's still running, though manager re-creation handles it
- // For more robust cleanup, FeatureFlagManager could have a .release() method
+ public JSONObject getRequestBodyAsJson() throws JSONException {
+ if (requestBodyBytes == null) return null;
+ return new JSONObject(new String(requestBodyBytes, StandardCharsets.UTF_8));
}
-
- // Helper method to create a valid flags JSON response string
- private String createFlagsResponseJson(Map flags) {
- JSONObject flagsObject = new JSONObject();
- try {
- for (Map.Entry entry : flags.entrySet()) {
- JSONObject flagDef = new JSONObject();
- flagDef.put("variant_key", entry.getValue().key);
- // Need to handle different types for value properly
- if (entry.getValue().value == null) {
- flagDef.put("variant_value", JSONObject.NULL);
- } else {
- flagDef.put("variant_value", entry.getValue().value);
- }
- flagsObject.put(entry.getKey(), flagDef);
- }
- return new JSONObject().put("flags", flagsObject).toString();
- } catch (JSONException e) {
- throw new RuntimeException("Error creating test JSON", e);
- }
+ }
+
+ private static class MockFeatureFlagDelegate implements FeatureFlagDelegate {
+ MPConfig configToReturn;
+ String distinctIdToReturn = TEST_DISTINCT_ID;
+ String tokenToReturn = TEST_TOKEN;
+ List trackCalls = new ArrayList<>();
+ CountDownLatch trackCalledLatch; // Optional: for tests waiting for track
+
+ static class TrackCall {
+ final String eventName;
+ final JSONObject properties;
+
+ TrackCall(String eventName, JSONObject properties) {
+ this.eventName = eventName;
+ this.properties = properties;
+ }
}
- // Helper to simulate MPConfig having specific FlagsConfig
- private void setupFlagsConfig(boolean enabled, @Nullable JSONObject context) {
- final JSONObject finalContext = (context == null) ? new JSONObject() : context;
- final FlagsConfig flagsConfig = new FlagsConfig(enabled, finalContext);
-
- mMockDelegate.configToReturn = new MPConfig(new Bundle(), mContext, TEST_TOKEN) {
- @Override
- public String getEventsEndpoint() { // Ensure server URL source
- return TEST_SERVER_URL + "/track/";
- }
-
- @Override
- public String getFlagsEndpoint() { // Ensure server URL source
- return TEST_SERVER_URL + "/flags/";
- }
- };
-
- mFeatureFlagManager = new FeatureFlagManager(
- mMockDelegate,
- mMockRemoteService,
- flagsConfig
- );
+ public MockFeatureFlagDelegate(MPConfig config) {
+ this.configToReturn = config;
}
- // ---- Test Cases ----
-
- @Test
- public void testAreFlagsReady_initialState() {
- assertFalse("Features should not be ready initially", mFeatureFlagManager.areFlagsReady());
+ @Override
+ public MPConfig getMPConfig() {
+ return configToReturn;
}
- @Test
- public void testLoadFlags_whenDisabled_doesNotFetch() throws InterruptedException {
- setupFlagsConfig(false, null); // Flags disabled
- mFeatureFlagManager.loadFlags();
-
- // Wait a bit to ensure no network call is attempted
- Thread.sleep(200); // Give handler thread time to process if it were to fetch
-
- CapturedRequest request = mMockRemoteService.takeRequest(100, TimeUnit.MILLISECONDS);
- assertNull("No network request should be made when flags are disabled", request);
- assertFalse("areFeaturesReady should be false", mFeatureFlagManager.areFlagsReady());
+ @Override
+ public String getDistinctId() {
+ return distinctIdToReturn;
}
- @Test
- public void testLoadFlags_whenEnabled_andFetchSucceeds_flagsBecomeReady() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject()); // Flags enabled
-
- Map testFlags = new HashMap<>();
- testFlags.put("flag1", new MixpanelFlagVariant("v1", true));
- String responseJson = createFlagsResponseJson(testFlags);
- mMockRemoteService.addResponse(responseJson.getBytes(StandardCharsets.UTF_8));
-
- mFeatureFlagManager.loadFlags();
-
- // Wait for fetch to complete (network call + handler processing)
- // Ideally, use CountDownLatch if loadFlags had a completion,
- // but for now, poll areFeaturesReady or wait a fixed time.
- boolean ready = false;
- for (int i = 0; i < 20; i++) { // Poll for up to 2 seconds
- if (mFeatureFlagManager.areFlagsReady()) {
- ready = true;
- break;
- }
- Thread.sleep(100);
- }
- assertTrue("Flags should become ready after successful fetch", ready);
-
- CapturedRequest request = mMockRemoteService.takeRequest(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertNotNull("A network request should have been made", request);
- assertTrue("Endpoint should be for flags", request.endpointUrl.endsWith("/flags/"));
+ @Override
+ public void track(String eventName, JSONObject properties) {
+ MPLog.v("FeatureFlagManagerTest", "MockDelegate.track called: " + eventName);
+ trackCalls.add(new TrackCall(eventName, properties));
+ if (trackCalledLatch != null) {
+ trackCalledLatch.countDown();
+ }
}
-
- @Test
- public void testLoadFlags_whenEnabled_andFetchFails_flagsNotReady() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- mMockRemoteService.addError(new IOException("Network unavailable"));
-
- mFeatureFlagManager.loadFlags();
-
- // Wait a bit to see if flags become ready (they shouldn't)
- Thread.sleep(500); // Enough time for the fetch attempt and failure processing
-
- assertFalse("Flags should not be ready after failed fetch", mFeatureFlagManager.areFlagsReady());
- CapturedRequest request = mMockRemoteService.takeRequest(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertNotNull("A network request should have been attempted", request);
+ @Override
+ public String getToken() {
+ return tokenToReturn;
}
- @Test
- public void testGetVariantSync_flagsNotReady_returnsFallback() {
- setupFlagsConfig(true, null); // Enabled, but no flags loaded yet
- assertFalse(mFeatureFlagManager.areFlagsReady());
-
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_key", "fb_value");
- MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("my_flag", fallback);
-
- assertEquals("Should return fallback key", fallback.key, result.key);
- assertEquals("Should return fallback value", fallback.value, result.value);
+ public void resetTrackCalls() {
+ trackCalls.clear();
+ trackCalledLatch = null;
}
+ }
- @Test
- public void testGetVariantSync_flagsReady_flagExists() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("test_flag", new MixpanelFlagVariant("variant_A", "hello"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- // Wait for flags to load
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("fallback_key", "fallback_value");
- MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("test_flag", fallback);
-
- assertEquals("Should return actual flag key", "variant_A", result.key);
- assertEquals("Should return actual flag value", "hello", result.value);
- }
+ private static class MockRemoteService implements RemoteService {
+ // Queue to hold responses/exceptions to be returned by performRequest
+ private final BlockingQueue mResults = new ArrayBlockingQueue<>(10);
+ // Queue to capture actual requests made
+ private final BlockingQueue mCapturedRequests = new ArrayBlockingQueue<>(10);
- @Test
- public void testGetVariantSync_flagsReady_flagMissing_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("another_flag", new MixpanelFlagVariant("variant_B", 123));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_key_sync", "fb_value_sync");
- MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("non_existent_flag", fallback);
-
- assertEquals("Should return fallback key", fallback.key, result.key);
- assertEquals("Should return fallback value", fallback.value, result.value);
+ @Override
+ public boolean isOnline(Context context, OfflineMode offlineMode) {
+ return true; // Assume online for tests unless specified
}
- @Test
- public void testGetVariant_Async_flagsReady_flagExists() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("async_flag", new MixpanelFlagVariant("v_async", true));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb", false);
-
- mFeatureFlagManager.getVariant("async_flag", fallback, result -> {
- resultRef.set(result);
- latch.countDown();
- });
-
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertEquals("v_async", resultRef.get().key);
- assertEquals(true, resultRef.get().value);
+ @Override
+ public void checkIsMixpanelBlocked() {
+ // No-op for tests
}
- @Test
- public void testGetVariant_Async_flagsNotReady_fetchSucceeds() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject()); // Enabled for fetch
- assertFalse(mFeatureFlagManager.areFlagsReady());
-
- Map serverFlags = new HashMap<>();
- serverFlags.put("fetch_flag_async", new MixpanelFlagVariant("fetched_variant", "fetched_value"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- // No loadFlags() call here, getFeature should trigger it
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_fetch", "fb_val_fetch");
-
- mFeatureFlagManager.getVariant("fetch_flag_async", fallback, result -> {
- resultRef.set(result);
- latch.countDown();
- });
-
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS)); // Longer timeout for fetch
- assertNotNull(resultRef.get());
- assertEquals("fetched_variant", resultRef.get().key);
- assertEquals("fetched_value", resultRef.get().value);
- assertTrue(mFeatureFlagManager.areFlagsReady());
+ @Override
+ public byte[] performRequest(
+ @NonNull String endpointUrl,
+ @Nullable ProxyServerInteractor interactor,
+ @Nullable Map params,
+ @Nullable Map headers,
+ @Nullable byte[] requestBodyBytes,
+ @Nullable SSLSocketFactory socketFactory)
+ throws ServiceUnavailableException, IOException {
+
+ mCapturedRequests.offer(new CapturedRequest(endpointUrl, headers, requestBodyBytes));
+
+ try {
+ Object result =
+ mResults.poll(FeatureFlagManagerTest.ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (result == null) {
+ throw new IOException("MockRemoteService timed out waiting for a result to be queued.");
+ }
+ if (result instanceof IOException) {
+ throw (IOException) result;
+ }
+ if (result instanceof ServiceUnavailableException) {
+ throw (ServiceUnavailableException) result;
+ }
+ if (result instanceof RuntimeException) { // For other test exceptions
+ throw (RuntimeException) result;
+ }
+ return (byte[]) result;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("MockRemoteService interrupted.", e);
+ }
}
- @Test
- public void testTracking_getVariantSync_calledOnce() throws InterruptedException, JSONException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("track_flag_sync", new MixpanelFlagVariant("v_track_sync", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- mMockDelegate.resetTrackCalls();
- mMockDelegate.trackCalledLatch = new CountDownLatch(1);
-
- MixpanelFlagVariant fallback = new MixpanelFlagVariant("", null);
- mFeatureFlagManager.getVariantSync("track_flag_sync", fallback); // First call, should track
- assertTrue("Track should have been called", mMockDelegate.trackCalledLatch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertEquals("Track should be called once", 1, mMockDelegate.trackCalls.size());
-
- // Second call, should NOT track again
- mFeatureFlagManager.getVariantSync("track_flag_sync", fallback);
- // Allow some time for potential erroneous track call
- Thread.sleep(200);
- assertEquals("Track should still be called only once", 1, mMockDelegate.trackCalls.size());
-
- MockFeatureFlagDelegate.TrackCall call = mMockDelegate.trackCalls.get(0);
- assertEquals("$experiment_started", call.eventName);
- assertEquals("track_flag_sync", call.properties.getString("Experiment name"));
- assertEquals("v_track_sync", call.properties.getString("Variant name"));
+ public void addResponse(byte[] responseBytes) {
+ mResults.offer(responseBytes);
}
- @Test
- public void testGetVariant_Async_flagsNotReady_fetchFails_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- assertFalse(mFeatureFlagManager.areFlagsReady());
-
- mMockRemoteService.addError(new IOException("Simulated fetch failure"));
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
- final MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_async_fail", "val_async_fail");
-
- mFeatureFlagManager.getVariant("some_flag_on_fail", fallback, result -> {
- resultRef.set(result);
- latch.countDown();
- });
-
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertEquals(fallback.key, resultRef.get().key);
- assertEquals(fallback.value, resultRef.get().value);
- assertFalse(mFeatureFlagManager.areFlagsReady());
- assertEquals(0, mMockDelegate.trackCalls.size()); // No tracking on fallback
+ public void addResponse(JSONObject responseJson) {
+ mResults.offer(responseJson.toString().getBytes(StandardCharsets.UTF_8));
}
- @Test
- public void testIsEnabledSync_flagsReady_flagExistsWithBooleanTrue() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("bool_flag_true", new MixpanelFlagVariant("enabled", true));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- boolean result = mFeatureFlagManager.isEnabledSync("bool_flag_true", false);
- assertTrue("Should return true when flag value is true", result);
+ public void addError(Exception e) {
+ mResults.offer(e);
}
- @Test
- public void testIsEnabledSync_flagsReady_flagExistsWithBooleanFalse() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("bool_flag_false", new MixpanelFlagVariant("disabled", false));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- boolean result = mFeatureFlagManager.isEnabledSync("bool_flag_false", true);
- assertFalse("Should return false when flag value is false", result);
+ public CapturedRequest takeRequest(long timeout, TimeUnit unit) throws InterruptedException {
+ return mCapturedRequests.poll(timeout, unit);
}
- @Test
- public void testIsEnabledSync_flagsReady_flagDoesNotExist_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("other_flag", new MixpanelFlagVariant("v1", "value"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- boolean result = mFeatureFlagManager.isEnabledSync("missing_bool_flag", true);
- assertTrue("Should return fallback value (true) when flag doesn't exist", result);
-
- boolean result2 = mFeatureFlagManager.isEnabledSync("missing_bool_flag", false);
- assertFalse("Should return fallback value (false) when flag doesn't exist", result2);
+ public void reset() {
+ mResults.clear();
+ mCapturedRequests.clear();
}
+ }
- @Test
- public void testIsEnabledSync_flagsNotReady_returnsFallback() {
- setupFlagsConfig(true, null);
- assertFalse(mFeatureFlagManager.areFlagsReady());
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ // MPConfig requires a context and a token (even if we override methods)
+ // Create a basic MPConfig. Specific flag settings will be set via MockDelegate.
+ mTestConfig = new MPConfig(new Bundle(), mContext, TEST_TOKEN);
- boolean result = mFeatureFlagManager.isEnabledSync("any_flag", true);
- assertTrue("Should return fallback value (true) when flags not ready", result);
+ mMockDelegate = new MockFeatureFlagDelegate(mTestConfig);
+ mMockRemoteService = new MockRemoteService();
- boolean result2 = mFeatureFlagManager.isEnabledSync("any_flag", false);
- assertFalse("Should return fallback value (false) when flags not ready", result2);
+ mFeatureFlagManager =
+ new FeatureFlagManager(
+ mMockDelegate, // Pass delegate directly, manager will wrap in WeakReference
+ mMockRemoteService,
+ new FlagsConfig(true, new JSONObject()));
+ MPLog.setLevel(MPLog.VERBOSE); // Enable verbose logging for tests
+ }
+
+ @After
+ public void tearDown() {
+ // Ensure handler thread is quit if it's still running, though manager re-creation handles it
+ // For more robust cleanup, FeatureFlagManager could have a .release() method
+ }
+
+ // Helper method to create a valid flags JSON response string
+ private String createFlagsResponseJson(Map flags) {
+ JSONObject flagsObject = new JSONObject();
+ try {
+ for (Map.Entry entry : flags.entrySet()) {
+ JSONObject flagDef = new JSONObject();
+ flagDef.put("variant_key", entry.getValue().key);
+ // Need to handle different types for value properly
+ if (entry.getValue().value == null) {
+ flagDef.put("variant_value", JSONObject.NULL);
+ } else {
+ flagDef.put("variant_value", entry.getValue().value);
+ }
+ flagsObject.put(entry.getKey(), flagDef);
+ }
+ return new JSONObject().put("flags", flagsObject).toString();
+ } catch (JSONException e) {
+ throw new RuntimeException("Error creating test JSON", e);
}
+ }
+
+ // Helper to simulate MPConfig having specific FlagsConfig
+ private void setupFlagsConfig(boolean enabled, @Nullable JSONObject context) {
+ final JSONObject finalContext = (context == null) ? new JSONObject() : context;
+ final FlagsConfig flagsConfig = new FlagsConfig(enabled, finalContext);
+
+ mMockDelegate.configToReturn =
+ new MPConfig(new Bundle(), mContext, TEST_TOKEN) {
+ @Override
+ public String getEventsEndpoint() { // Ensure server URL source
+ return TEST_SERVER_URL + "/track/";
+ }
+
+ @Override
+ public String getFlagsEndpoint() { // Ensure server URL source
+ return TEST_SERVER_URL + "/flags/";
+ }
+ };
- @Test
- public void testIsEnabledSync_flagsReady_nonBooleanValue_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("string_flag", new MixpanelFlagVariant("v1", "not_a_boolean"));
- serverFlags.put("number_flag", new MixpanelFlagVariant("v2", 123));
- serverFlags.put("null_flag", new MixpanelFlagVariant("v3", null));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- assertTrue("String value should return fallback true", mFeatureFlagManager.isEnabledSync("string_flag", true));
- assertFalse("String value should return fallback false", mFeatureFlagManager.isEnabledSync("string_flag", false));
-
- assertTrue("Number value should return fallback true", mFeatureFlagManager.isEnabledSync("number_flag", true));
- assertFalse("Number value should return fallback false", mFeatureFlagManager.isEnabledSync("number_flag", false));
-
- assertTrue("Null value should return fallback true", mFeatureFlagManager.isEnabledSync("null_flag", true));
- assertFalse("Null value should return fallback false", mFeatureFlagManager.isEnabledSync("null_flag", false));
+ mFeatureFlagManager = new FeatureFlagManager(mMockDelegate, mMockRemoteService, flagsConfig);
+ }
+
+ // ---- Test Cases ----
+
+ @Test
+ public void testAreFlagsReady_initialState() {
+ assertFalse("Features should not be ready initially", mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testLoadFlags_whenDisabled_doesNotFetch() throws InterruptedException {
+ setupFlagsConfig(false, null); // Flags disabled
+ mFeatureFlagManager.loadFlags();
+
+ // Wait a bit to ensure no network call is attempted
+ Thread.sleep(200); // Give handler thread time to process if it were to fetch
+
+ CapturedRequest request = mMockRemoteService.takeRequest(100, TimeUnit.MILLISECONDS);
+ assertNull("No network request should be made when flags are disabled", request);
+ assertFalse("areFeaturesReady should be false", mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testLoadFlags_whenEnabled_andFetchSucceeds_flagsBecomeReady()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject()); // Flags enabled
+
+ Map testFlags = new HashMap<>();
+ testFlags.put("flag1", new MixpanelFlagVariant("v1", true));
+ String responseJson = createFlagsResponseJson(testFlags);
+ mMockRemoteService.addResponse(responseJson.getBytes(StandardCharsets.UTF_8));
+
+ mFeatureFlagManager.loadFlags();
+
+ // Wait for fetch to complete (network call + handler processing)
+ // Ideally, use CountDownLatch if loadFlags had a completion,
+ // but for now, poll areFeaturesReady or wait a fixed time.
+ boolean ready = false;
+ for (int i = 0; i < 20; i++) { // Poll for up to 2 seconds
+ if (mFeatureFlagManager.areFlagsReady()) {
+ ready = true;
+ break;
+ }
+ Thread.sleep(100);
}
-
- @Test
- public void testIsEnabled_Async_flagsReady_booleanTrue() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("async_bool_true", new MixpanelFlagVariant("v_true", true));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
-
- mFeatureFlagManager.isEnabled("async_bool_true", false, result -> {
- resultRef.set(result);
- latch.countDown();
+ assertTrue("Flags should become ready after successful fetch", ready);
+
+ CapturedRequest request =
+ mMockRemoteService.takeRequest(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assertNotNull("A network request should have been made", request);
+ assertTrue("Endpoint should be for flags", request.endpointUrl.endsWith("/flags/"));
+ }
+
+ @Test
+ public void testLoadFlags_whenEnabled_andFetchFails_flagsNotReady() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ mMockRemoteService.addError(new IOException("Network unavailable"));
+
+ mFeatureFlagManager.loadFlags();
+
+ // Wait a bit to see if flags become ready (they shouldn't)
+ Thread.sleep(500); // Enough time for the fetch attempt and failure processing
+
+ assertFalse(
+ "Flags should not be ready after failed fetch", mFeatureFlagManager.areFlagsReady());
+ CapturedRequest request =
+ mMockRemoteService.takeRequest(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assertNotNull("A network request should have been attempted", request);
+ }
+
+ @Test
+ public void testGetVariantSync_flagsNotReady_returnsFallback() {
+ setupFlagsConfig(true, null); // Enabled, but no flags loaded yet
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_key", "fb_value");
+ MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("my_flag", fallback);
+
+ assertEquals("Should return fallback key", fallback.key, result.key);
+ assertEquals("Should return fallback value", fallback.value, result.value);
+ }
+
+ @Test
+ public void testGetVariantSync_flagsReady_flagExists() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("test_flag", new MixpanelFlagVariant("variant_A", "hello"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ // Wait for flags to load
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("fallback_key", "fallback_value");
+ MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("test_flag", fallback);
+
+ assertEquals("Should return actual flag key", "variant_A", result.key);
+ assertEquals("Should return actual flag value", "hello", result.value);
+ }
+
+ @Test
+ public void testGetVariantSync_flagsReady_flagMissing_returnsFallback()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("another_flag", new MixpanelFlagVariant("variant_B", 123));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_key_sync", "fb_value_sync");
+ MixpanelFlagVariant result = mFeatureFlagManager.getVariantSync("non_existent_flag", fallback);
+
+ assertEquals("Should return fallback key", fallback.key, result.key);
+ assertEquals("Should return fallback value", fallback.value, result.value);
+ }
+
+ @Test
+ public void testGetVariant_Async_flagsReady_flagExists() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("async_flag", new MixpanelFlagVariant("v_async", true));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb", false);
+
+ mFeatureFlagManager.getVariant(
+ "async_flag",
+ fallback,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
});
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertTrue("Should return true for boolean true flag", resultRef.get());
- }
-
- @Test
- public void testIsEnabled_Async_flagsReady_booleanFalse() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("async_bool_false", new MixpanelFlagVariant("v_false", false));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
-
- mFeatureFlagManager.isEnabled("async_bool_false", true, result -> {
- resultRef.set(result);
- latch.countDown();
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertEquals("v_async", resultRef.get().key);
+ assertEquals(true, resultRef.get().value);
+ }
+
+ @Test
+ public void testGetVariant_Async_flagsNotReady_fetchSucceeds() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject()); // Enabled for fetch
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ Map serverFlags = new HashMap<>();
+ serverFlags.put(
+ "fetch_flag_async", new MixpanelFlagVariant("fetched_variant", "fetched_value"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ // No loadFlags() call here, getFeature should trigger it
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_fetch", "fb_val_fetch");
+
+ mFeatureFlagManager.getVariant(
+ "fetch_flag_async",
+ fallback,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
});
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertFalse("Should return false for boolean false flag", resultRef.get());
- }
-
- @Test
- public void testIsEnabled_Async_flagsNotReady_fetchSucceeds() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- assertFalse(mFeatureFlagManager.areFlagsReady());
-
- Map serverFlags = new HashMap<>();
- serverFlags.put("fetch_bool_flag", new MixpanelFlagVariant("fetched", true));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
-
- mFeatureFlagManager.isEnabled("fetch_bool_flag", false, result -> {
- resultRef.set(result);
- latch.countDown();
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS)); // Longer timeout for fetch
+ assertNotNull(resultRef.get());
+ assertEquals("fetched_variant", resultRef.get().key);
+ assertEquals("fetched_value", resultRef.get().value);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testTracking_getVariantSync_calledOnce() throws InterruptedException, JSONException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("track_flag_sync", new MixpanelFlagVariant("v_track_sync", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ mMockDelegate.resetTrackCalls();
+ mMockDelegate.trackCalledLatch = new CountDownLatch(1);
+
+ MixpanelFlagVariant fallback = new MixpanelFlagVariant("", null);
+ mFeatureFlagManager.getVariantSync("track_flag_sync", fallback); // First call, should track
+ assertTrue(
+ "Track should have been called",
+ mMockDelegate.trackCalledLatch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals("Track should be called once", 1, mMockDelegate.trackCalls.size());
+
+ // Second call, should NOT track again
+ mFeatureFlagManager.getVariantSync("track_flag_sync", fallback);
+ // Allow some time for potential erroneous track call
+ Thread.sleep(200);
+ assertEquals("Track should still be called only once", 1, mMockDelegate.trackCalls.size());
+
+ MockFeatureFlagDelegate.TrackCall call = mMockDelegate.trackCalls.get(0);
+ assertEquals("$experiment_started", call.eventName);
+ assertEquals("track_flag_sync", call.properties.getString("Experiment name"));
+ assertEquals("v_track_sync", call.properties.getString("Variant name"));
+ }
+
+ @Test
+ public void testGetVariant_Async_flagsNotReady_fetchFails_returnsFallback()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ mMockRemoteService.addError(new IOException("Simulated fetch failure"));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+ final MixpanelFlagVariant fallback = new MixpanelFlagVariant("fb_async_fail", "val_async_fail");
+
+ mFeatureFlagManager.getVariant(
+ "some_flag_on_fail",
+ fallback,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
});
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertTrue("Should return true after successful fetch", resultRef.get());
- assertTrue(mFeatureFlagManager.areFlagsReady());
- }
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertEquals(fallback.key, resultRef.get().key);
+ assertEquals(fallback.value, resultRef.get().value);
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+ assertEquals(0, mMockDelegate.trackCalls.size()); // No tracking on fallback
+ }
+
+ @Test
+ public void testIsEnabledSync_flagsReady_flagExistsWithBooleanTrue() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("bool_flag_true", new MixpanelFlagVariant("enabled", true));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ boolean result = mFeatureFlagManager.isEnabledSync("bool_flag_true", false);
+ assertTrue("Should return true when flag value is true", result);
+ }
+
+ @Test
+ public void testIsEnabledSync_flagsReady_flagExistsWithBooleanFalse()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("bool_flag_false", new MixpanelFlagVariant("disabled", false));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ boolean result = mFeatureFlagManager.isEnabledSync("bool_flag_false", true);
+ assertFalse("Should return false when flag value is false", result);
+ }
+
+ @Test
+ public void testIsEnabledSync_flagsReady_flagDoesNotExist_returnsFallback()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("other_flag", new MixpanelFlagVariant("v1", "value"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ boolean result = mFeatureFlagManager.isEnabledSync("missing_bool_flag", true);
+ assertTrue("Should return fallback value (true) when flag doesn't exist", result);
+
+ boolean result2 = mFeatureFlagManager.isEnabledSync("missing_bool_flag", false);
+ assertFalse("Should return fallback value (false) when flag doesn't exist", result2);
+ }
+
+ @Test
+ public void testIsEnabledSync_flagsNotReady_returnsFallback() {
+ setupFlagsConfig(true, null);
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ boolean result = mFeatureFlagManager.isEnabledSync("any_flag", true);
+ assertTrue("Should return fallback value (true) when flags not ready", result);
+
+ boolean result2 = mFeatureFlagManager.isEnabledSync("any_flag", false);
+ assertFalse("Should return fallback value (false) when flags not ready", result2);
+ }
+
+ @Test
+ public void testIsEnabledSync_flagsReady_nonBooleanValue_returnsFallback()
+ throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("string_flag", new MixpanelFlagVariant("v1", "not_a_boolean"));
+ serverFlags.put("number_flag", new MixpanelFlagVariant("v2", 123));
+ serverFlags.put("null_flag", new MixpanelFlagVariant("v3", null));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ assertTrue(
+ "String value should return fallback true",
+ mFeatureFlagManager.isEnabledSync("string_flag", true));
+ assertFalse(
+ "String value should return fallback false",
+ mFeatureFlagManager.isEnabledSync("string_flag", false));
+
+ assertTrue(
+ "Number value should return fallback true",
+ mFeatureFlagManager.isEnabledSync("number_flag", true));
+ assertFalse(
+ "Number value should return fallback false",
+ mFeatureFlagManager.isEnabledSync("number_flag", false));
+
+ assertTrue(
+ "Null value should return fallback true",
+ mFeatureFlagManager.isEnabledSync("null_flag", true));
+ assertFalse(
+ "Null value should return fallback false",
+ mFeatureFlagManager.isEnabledSync("null_flag", false));
+ }
+
+ @Test
+ public void testIsEnabled_Async_flagsReady_booleanTrue() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("async_bool_true", new MixpanelFlagVariant("v_true", true));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+
+ mFeatureFlagManager.isEnabled(
+ "async_bool_true",
+ false,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
+ });
- @Test
- public void testIsEnabled_Async_nonBooleanValue_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- Map serverFlags = new HashMap<>();
- serverFlags.put("string_async", new MixpanelFlagVariant("v_str", "hello"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
- mFeatureFlagManager.loadFlags();
- for(int i = 0; i<20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
- assertTrue(mFeatureFlagManager.areFlagsReady());
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
-
- mFeatureFlagManager.isEnabled("string_async", true, result -> {
- resultRef.set(result);
- latch.countDown();
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertTrue("Should return true for boolean true flag", resultRef.get());
+ }
+
+ @Test
+ public void testIsEnabled_Async_flagsReady_booleanFalse() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("async_bool_false", new MixpanelFlagVariant("v_false", false));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+
+ mFeatureFlagManager.isEnabled(
+ "async_bool_false",
+ true,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
});
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertTrue("Should return fallback (true) for non-boolean value", resultRef.get());
- }
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertFalse("Should return false for boolean false flag", resultRef.get());
+ }
+
+ @Test
+ public void testIsEnabled_Async_flagsNotReady_fetchSucceeds() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("fetch_bool_flag", new MixpanelFlagVariant("fetched", true));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+
+ mFeatureFlagManager.isEnabled(
+ "fetch_bool_flag",
+ false,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
+ });
- @Test
- public void testIsEnabled_Async_fetchFails_returnsFallback() throws InterruptedException {
- setupFlagsConfig(true, new JSONObject());
- assertFalse(mFeatureFlagManager.areFlagsReady());
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertTrue("Should return true after successful fetch", resultRef.get());
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testIsEnabled_Async_nonBooleanValue_returnsFallback() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("string_async", new MixpanelFlagVariant("v_str", "hello"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+ mFeatureFlagManager.loadFlags();
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); ++i) Thread.sleep(100);
+ assertTrue(mFeatureFlagManager.areFlagsReady());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+
+ mFeatureFlagManager.isEnabled(
+ "string_async",
+ true,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
+ });
- mMockRemoteService.addError(new IOException("Network error"));
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertTrue("Should return fallback (true) for non-boolean value", resultRef.get());
+ }
+
+ @Test
+ public void testIsEnabled_Async_fetchFails_returnsFallback() throws InterruptedException {
+ setupFlagsConfig(true, new JSONObject());
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+
+ mMockRemoteService.addError(new IOException("Network error"));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference resultRef = new AtomicReference<>();
+
+ mFeatureFlagManager.isEnabled(
+ "fail_flag",
+ true,
+ result -> {
+ resultRef.set(result);
+ latch.countDown();
+ });
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference resultRef = new AtomicReference<>();
+ assertTrue(
+ "Callback should complete within timeout",
+ latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
+ assertNotNull(resultRef.get());
+ assertTrue("Should return fallback (true) when fetch fails", resultRef.get());
+ assertFalse(mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testConcurrentLoadFlagsCalls() throws InterruptedException {
+ // Setup with flags enabled
+ setupFlagsConfig(true, new JSONObject());
+
+ // Track number of network requests made
+ final AtomicInteger requestCount = new AtomicInteger(0);
+
+ // Prepare response data
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("concurrent_flag", new MixpanelFlagVariant("test_variant", "test_value"));
+
+ // Create a custom MockRemoteService that counts requests and introduces delay
+ MockRemoteService customMockService =
+ new MockRemoteService() {
+ @Override
+ public byte[] performRequest(
+ String endpointUrl,
+ ProxyServerInteractor interactor,
+ Map params,
+ Map headers,
+ byte[] requestBodyBytes,
+ SSLSocketFactory socketFactory)
+ throws ServiceUnavailableException, IOException {
+ // Count the request
+ requestCount.incrementAndGet();
+
+ // Introduce a delay to simulate network latency
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
- mFeatureFlagManager.isEnabled("fail_flag", true, result -> {
- resultRef.set(result);
- latch.countDown();
- });
+ // Return the prepared response
+ return super.performRequest(
+ endpointUrl, interactor, params, headers, requestBodyBytes, socketFactory);
+ }
+ };
- assertTrue("Callback should complete within timeout", latch.await(ASYNC_TEST_TIMEOUT_MS * 2, TimeUnit.MILLISECONDS));
- assertNotNull(resultRef.get());
- assertTrue("Should return fallback (true) when fetch fails", resultRef.get());
- assertFalse(mFeatureFlagManager.areFlagsReady());
+ // Add response to the custom mock service
+ customMockService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Use reflection to set the custom mock service
+ try {
+ Field httpServiceField = FeatureFlagManager.class.getDeclaredField("mHttpService");
+ httpServiceField.setAccessible(true);
+ httpServiceField.set(mFeatureFlagManager, customMockService);
+ } catch (Exception e) {
+ fail("Failed to set mock http service: " + e.getMessage());
}
- @Test
- public void testConcurrentLoadFlagsCalls() throws InterruptedException {
- // Setup with flags enabled
- setupFlagsConfig(true, new JSONObject());
-
- // Track number of network requests made
- final AtomicInteger requestCount = new AtomicInteger(0);
-
- // Prepare response data
- Map serverFlags = new HashMap<>();
- serverFlags.put("concurrent_flag", new MixpanelFlagVariant("test_variant", "test_value"));
-
- // Create a custom MockRemoteService that counts requests and introduces delay
- MockRemoteService customMockService = new MockRemoteService() {
- @Override
- public byte[] performRequest(String endpointUrl, ProxyServerInteractor interactor,
- Map params, Map headers,
- byte[] requestBodyBytes, SSLSocketFactory socketFactory)
- throws ServiceUnavailableException, IOException {
- // Count the request
- requestCount.incrementAndGet();
-
- // Introduce a delay to simulate network latency
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- // Return the prepared response
- return super.performRequest(endpointUrl, interactor, params, headers, requestBodyBytes, socketFactory);
- }
- };
-
- // Add response to the custom mock service
- customMockService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Use reflection to set the custom mock service
- try {
- Field httpServiceField = FeatureFlagManager.class.getDeclaredField("mHttpService");
- httpServiceField.setAccessible(true);
- httpServiceField.set(mFeatureFlagManager, customMockService);
- } catch (Exception e) {
- fail("Failed to set mock http service: " + e.getMessage());
- }
-
- // Number of concurrent threads
- final int threadCount = 10;
- final CountDownLatch startLatch = new CountDownLatch(1);
- final CountDownLatch completionLatch = new CountDownLatch(threadCount);
- final List threads = new ArrayList<>();
- final AtomicInteger successCount = new AtomicInteger(0);
-
- // Create multiple threads that will call loadFlags concurrently
- for (int i = 0; i < threadCount; i++) {
- Thread thread = new Thread(() -> {
+ // Number of concurrent threads
+ final int threadCount = 10;
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch completionLatch = new CountDownLatch(threadCount);
+ final List threads = new ArrayList<>();
+ final AtomicInteger successCount = new AtomicInteger(0);
+
+ // Create multiple threads that will call loadFlags concurrently
+ for (int i = 0; i < threadCount; i++) {
+ Thread thread =
+ new Thread(
+ () -> {
try {
- // Wait for signal to start all threads simultaneously
- startLatch.await();
- // Call loadFlags
- mFeatureFlagManager.loadFlags();
- successCount.incrementAndGet();
+ // Wait for signal to start all threads simultaneously
+ startLatch.await();
+ // Call loadFlags
+ mFeatureFlagManager.loadFlags();
+ successCount.incrementAndGet();
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ Thread.currentThread().interrupt();
} finally {
- completionLatch.countDown();
+ completionLatch.countDown();
}
- });
- threads.add(thread);
- thread.start();
- }
-
- // Start all threads at the same time
- startLatch.countDown();
-
- // Wait for all threads to complete
- assertTrue("All threads should complete within timeout",
- completionLatch.await(5000, TimeUnit.MILLISECONDS));
-
- // Wait a bit more for all loadFlags operations to complete
- Thread.sleep(500);
-
- // Verify results
- assertEquals("All threads should have completed successfully", threadCount, successCount.get());
-
- // Only one network request should have been made despite multiple concurrent calls
- // This verifies that loadFlags properly handles concurrent calls
- assertEquals("Should only make one network request for concurrent loadFlags calls",
- 1, requestCount.get());
-
- // Verify flags are ready
- assertTrue("Flags should be ready after concurrent loads", mFeatureFlagManager.areFlagsReady());
-
- // Test accessing the flag synchronously
- MixpanelFlagVariant variant = mFeatureFlagManager.getVariantSync("concurrent_flag",
- new MixpanelFlagVariant("default"));
- assertNotNull("Flag variant should not be null", variant);
- assertEquals("test_variant", variant.key);
- assertEquals("test_value", variant.value);
+ });
+ threads.add(thread);
+ thread.start();
}
- @Test
- public void testConcurrentGetVariantCalls_whenFlagsNotReady() throws InterruptedException {
- // Setup with flags enabled
- setupFlagsConfig(true, new JSONObject());
-
- // Prepare response data that will be delayed
- Map serverFlags = new HashMap<>();
- serverFlags.put("concurrent_get_flag1", new MixpanelFlagVariant("variant1", "value1"));
- serverFlags.put("concurrent_get_flag2", new MixpanelFlagVariant("variant2", "value2"));
- serverFlags.put("concurrent_get_flag3", new MixpanelFlagVariant("variant3", "value3"));
-
- // Create a mock service that introduces significant delay to simulate slow network
- final AtomicInteger requestCount = new AtomicInteger(0);
- MockRemoteService delayedMockService = new MockRemoteService() {
- @Override
- public byte[] performRequest(String endpointUrl, ProxyServerInteractor interactor,
- Map params, Map headers,
- byte[] requestBodyBytes, SSLSocketFactory socketFactory)
- throws ServiceUnavailableException, IOException {
- requestCount.incrementAndGet();
- // Introduce significant delay to ensure getVariant calls happen before flags are ready
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- return super.performRequest(endpointUrl, interactor, params, headers, requestBodyBytes, socketFactory);
+ // Start all threads at the same time
+ startLatch.countDown();
+
+ // Wait for all threads to complete
+ assertTrue(
+ "All threads should complete within timeout",
+ completionLatch.await(5000, TimeUnit.MILLISECONDS));
+
+ // Wait a bit more for all loadFlags operations to complete
+ Thread.sleep(500);
+
+ // Verify results
+ assertEquals("All threads should have completed successfully", threadCount, successCount.get());
+
+ // Only one network request should have been made despite multiple concurrent calls
+ // This verifies that loadFlags properly handles concurrent calls
+ assertEquals(
+ "Should only make one network request for concurrent loadFlags calls",
+ 1,
+ requestCount.get());
+
+ // Verify flags are ready
+ assertTrue("Flags should be ready after concurrent loads", mFeatureFlagManager.areFlagsReady());
+
+ // Test accessing the flag synchronously
+ MixpanelFlagVariant variant =
+ mFeatureFlagManager.getVariantSync("concurrent_flag", new MixpanelFlagVariant("default"));
+ assertNotNull("Flag variant should not be null", variant);
+ assertEquals("test_variant", variant.key);
+ assertEquals("test_value", variant.value);
+ }
+
+ @Test
+ public void testConcurrentGetVariantCalls_whenFlagsNotReady() throws InterruptedException {
+ // Setup with flags enabled
+ setupFlagsConfig(true, new JSONObject());
+
+ // Prepare response data that will be delayed
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("concurrent_get_flag1", new MixpanelFlagVariant("variant1", "value1"));
+ serverFlags.put("concurrent_get_flag2", new MixpanelFlagVariant("variant2", "value2"));
+ serverFlags.put("concurrent_get_flag3", new MixpanelFlagVariant("variant3", "value3"));
+
+ // Create a mock service that introduces significant delay to simulate slow network
+ final AtomicInteger requestCount = new AtomicInteger(0);
+ MockRemoteService delayedMockService =
+ new MockRemoteService() {
+ @Override
+ public byte[] performRequest(
+ String endpointUrl,
+ ProxyServerInteractor interactor,
+ Map params,
+ Map headers,
+ byte[] requestBodyBytes,
+ SSLSocketFactory socketFactory)
+ throws ServiceUnavailableException, IOException {
+ requestCount.incrementAndGet();
+ // Introduce significant delay to ensure getVariant calls happen before flags are ready
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
+ return super.performRequest(
+ endpointUrl, interactor, params, headers, requestBodyBytes, socketFactory);
+ }
};
-
- // Add response to the mock service
- delayedMockService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Use reflection to set the custom mock service
- try {
- Field httpServiceField = FeatureFlagManager.class.getDeclaredField("mHttpService");
- httpServiceField.setAccessible(true);
- httpServiceField.set(mFeatureFlagManager, delayedMockService);
- } catch (Exception e) {
- fail("Failed to set mock http service: " + e.getMessage());
- }
-
- // Trigger loadFlags which will be delayed
- mFeatureFlagManager.loadFlags();
-
- // Verify flags are not ready yet
- assertFalse("Flags should not be ready immediately after loadFlags", mFeatureFlagManager.areFlagsReady());
-
- // Number of concurrent threads calling getVariant
- final int threadCount = 20;
- final CountDownLatch startLatch = new CountDownLatch(1);
- final CountDownLatch completionLatch = new CountDownLatch(threadCount);
- final List threads = new ArrayList<>();
- final Map results = new HashMap<>();
- final AtomicInteger successCount = new AtomicInteger(0);
-
- // Create multiple threads that will call getVariant concurrently while flags are loading
- for (int i = 0; i < threadCount; i++) {
- final int threadIndex = i;
- final String flagName = "concurrent_get_flag" + ((i % 3) + 1); // Rotate through 3 different flags
- final MixpanelFlagVariant fallback = new MixpanelFlagVariant("fallback" + threadIndex, "fallback_value" + threadIndex);
-
- Thread thread = new Thread(() -> {
+
+ // Add response to the mock service
+ delayedMockService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Use reflection to set the custom mock service
+ try {
+ Field httpServiceField = FeatureFlagManager.class.getDeclaredField("mHttpService");
+ httpServiceField.setAccessible(true);
+ httpServiceField.set(mFeatureFlagManager, delayedMockService);
+ } catch (Exception e) {
+ fail("Failed to set mock http service: " + e.getMessage());
+ }
+
+ // Trigger loadFlags which will be delayed
+ mFeatureFlagManager.loadFlags();
+
+ // Verify flags are not ready yet
+ assertFalse(
+ "Flags should not be ready immediately after loadFlags",
+ mFeatureFlagManager.areFlagsReady());
+
+ // Number of concurrent threads calling getVariant
+ final int threadCount = 20;
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch completionLatch = new CountDownLatch(threadCount);
+ final List threads = new ArrayList<>();
+ final Map results = new HashMap<>();
+ final AtomicInteger successCount = new AtomicInteger(0);
+
+ // Create multiple threads that will call getVariant concurrently while flags are loading
+ for (int i = 0; i < threadCount; i++) {
+ final int threadIndex = i;
+ final String flagName =
+ "concurrent_get_flag" + ((i % 3) + 1); // Rotate through 3 different flags
+ final MixpanelFlagVariant fallback =
+ new MixpanelFlagVariant("fallback" + threadIndex, "fallback_value" + threadIndex);
+
+ Thread thread =
+ new Thread(
+ () -> {
try {
- // Wait for signal to start all threads simultaneously
- startLatch.await();
-
- // Use async getVariant with callback
- final CountDownLatch variantLatch = new CountDownLatch(1);
- final AtomicReference variantRef = new AtomicReference<>();
-
- mFeatureFlagManager.getVariant(flagName, fallback, variant -> {
+ // Wait for signal to start all threads simultaneously
+ startLatch.await();
+
+ // Use async getVariant with callback
+ final CountDownLatch variantLatch = new CountDownLatch(1);
+ final AtomicReference variantRef = new AtomicReference<>();
+
+ mFeatureFlagManager.getVariant(
+ flagName,
+ fallback,
+ variant -> {
variantRef.set(variant);
variantLatch.countDown();
- });
-
- // Wait for callback
- if (variantLatch.await(2000, TimeUnit.MILLISECONDS)) {
- results.put(threadIndex, variantRef.get());
- successCount.incrementAndGet();
- }
+ });
+
+ // Wait for callback
+ if (variantLatch.await(2000, TimeUnit.MILLISECONDS)) {
+ results.put(threadIndex, variantRef.get());
+ successCount.incrementAndGet();
+ }
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ Thread.currentThread().interrupt();
} finally {
- completionLatch.countDown();
+ completionLatch.countDown();
}
- });
- threads.add(thread);
- thread.start();
- }
-
- // Start all threads at the same time (while flags are still loading)
- startLatch.countDown();
-
- // Wait for all threads to complete
- assertTrue("All threads should complete within timeout",
- completionLatch.await(3000, TimeUnit.MILLISECONDS));
-
- // Verify results
- assertEquals("All threads should have completed successfully", threadCount, successCount.get());
-
- // Only one network request should have been made
- assertEquals("Should only make one network request", 1, requestCount.get());
-
- // Verify flags are now ready
- assertTrue("Flags should be ready after all getVariant calls complete", mFeatureFlagManager.areFlagsReady());
-
- // Verify all threads got the correct values (not fallbacks)
- for (int i = 0; i < threadCount; i++) {
- MixpanelFlagVariant result = results.get(i);
- assertNotNull("Thread " + i + " should have a result", result);
-
- int flagIndex = (i % 3) + 1;
- String expectedKey = "variant" + flagIndex;
- String expectedValue = "value" + flagIndex;
-
- assertEquals("Thread " + i + " should have correct variant key", expectedKey, result.key);
- assertEquals("Thread " + i + " should have correct variant value", expectedValue, result.value);
-
- // Verify it's not the fallback
- assertNotEquals("Thread " + i + " should not have fallback key", "fallback" + i, result.key);
- }
+ });
+ threads.add(thread);
+ thread.start();
}
- @Test
- public void testRequestBodyConstruction_performFetchRequest() throws InterruptedException, JSONException {
- // Setup with flags enabled and specific context data
- JSONObject contextData = new JSONObject();
- contextData.put("$os", "Android");
- contextData.put("$os_version", "13");
- contextData.put("custom_property", "test_value");
- setupFlagsConfig(true, contextData);
-
- // Set distinct ID for the request
- mMockDelegate.distinctIdToReturn = "test_user_123";
-
- // Create response data
- Map serverFlags = new HashMap<>();
- serverFlags.put("test_flag", new MixpanelFlagVariant("variant_a", "value_a"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger loadFlags to initiate the request
- mFeatureFlagManager.loadFlags();
-
- // Capture the request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- assertNotNull("Request should have been made", capturedRequest);
-
- // Verify the endpoint URL
- assertTrue("URL should contain /flags endpoint", capturedRequest.endpointUrl.contains("/flags"));
-
- // Parse and verify the request body
- assertNotNull("Request should have body", capturedRequest.requestBodyBytes);
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
- assertNotNull("Request body should be valid JSON", requestBody);
-
- // Log the actual request body for debugging
- MPLog.v("FeatureFlagManagerTest", "Request body: " + requestBody.toString());
-
- // Verify context is included
- assertTrue("Request should contain context", requestBody.has("context"));
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // Verify distinct_id is in the context
- assertTrue("Context should contain distinct_id", requestContext.has("distinct_id"));
- assertEquals("Context should contain correct distinct_id",
- "test_user_123", requestContext.getString("distinct_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"));
- assertEquals("Context should contain custom_property", "test_value", requestContext.getString("custom_property"));
-
- // Verify headers
- assertNotNull("Request should have headers", capturedRequest.headers);
- assertEquals("Content-Type should be application/json with charset",
- "application/json; charset=utf-8", capturedRequest.headers.get("Content-Type"));
-
- // 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());
- }
-
- @Test
- public void testRequestBodyConstruction_withNullContext() throws InterruptedException, JSONException {
- // Setup with flags enabled but null context
- setupFlagsConfig(true, null);
-
- // Set distinct ID
- mMockDelegate.distinctIdToReturn = "user_456";
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- assertNotNull("Request should have been made", capturedRequest);
-
- // Parse request body
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
-
- // Verify context exists
- assertTrue("Request should contain context", requestBody.has("context"));
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // 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());
- }
-
- @Test
- public void testRequestBodyConstruction_withEmptyDistinctId() throws InterruptedException, JSONException {
- // Setup with flags enabled
- setupFlagsConfig(true, new JSONObject());
-
- // Set empty distinct ID
- mMockDelegate.distinctIdToReturn = "";
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- assertNotNull("Request should have been made", capturedRequest);
-
- // Parse request body
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
-
- // Verify context exists
- assertTrue("Request should contain context", requestBody.has("context"));
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // 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"));
- }
+ // Start all threads at the same time (while flags are still loading)
+ startLatch.countDown();
- @Test
- public void testFlagsConfigContextUsage_initialContext() throws InterruptedException, JSONException {
- // Test that initial context from FlagsConfig is properly used
- JSONObject initialContext = new JSONObject();
- initialContext.put("app_version", "1.0.0");
- initialContext.put("platform", "Android");
- initialContext.put("custom_prop", "initial_value");
-
- setupFlagsConfig(true, initialContext);
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("test_flag", new MixpanelFlagVariant("v1", "value1"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture and verify request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- assertNotNull("Request should have been made", capturedRequest);
-
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // Verify all initial context properties are included
- assertEquals("app_version should be preserved", "1.0.0", requestContext.getString("app_version"));
- assertEquals("platform should be preserved", "Android", requestContext.getString("platform"));
- assertEquals("custom_prop should be preserved", "initial_value", requestContext.getString("custom_prop"));
-
- // Verify distinct_id is added to context
- assertTrue("distinct_id should be added to context", requestContext.has("distinct_id"));
- }
-
- @Test
- public void testFlagsConfigContextUsage_contextMerging() throws InterruptedException, JSONException {
- // Test that distinct_id doesn't override existing context properties
- JSONObject initialContext = new JSONObject();
- initialContext.put("distinct_id", "should_be_overridden"); // This should be overridden
- initialContext.put("user_type", "premium");
- initialContext.put("$os", "Android");
-
- setupFlagsConfig(true, initialContext);
-
- // Set a different distinct_id via delegate
- mMockDelegate.distinctIdToReturn = "actual_distinct_id";
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture and verify request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // Verify distinct_id from delegate overrides the one in initial context
- assertEquals("distinct_id should be from delegate, not initial context",
- "actual_distinct_id", requestContext.getString("distinct_id"));
-
- // Verify other properties are preserved
- assertEquals("user_type should be preserved", "premium", requestContext.getString("user_type"));
- assertEquals("$os should be preserved", "Android", requestContext.getString("$os"));
- }
-
- @Test
- public void testFlagsConfigContextUsage_emptyContext() throws InterruptedException, JSONException {
- // Test behavior with empty context object
- JSONObject emptyContext = new JSONObject();
- setupFlagsConfig(true, emptyContext);
-
- mMockDelegate.distinctIdToReturn = "test_user";
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture and verify request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- 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());
- assertEquals("distinct_id should be present", "test_user", requestContext.getString("distinct_id"));
- }
-
- @Test
- public void testFlagsConfigContextUsage_complexNestedContext() throws InterruptedException, JSONException {
- // Test that complex nested objects in context are preserved
- JSONObject nestedObj = new JSONObject();
- nestedObj.put("city", "San Francisco");
- nestedObj.put("country", "USA");
-
- JSONObject initialContext = new JSONObject();
- initialContext.put("location", nestedObj);
- initialContext.put("features_enabled", new JSONArray().put("feature1").put("feature2"));
- initialContext.put("is_beta", true);
- initialContext.put("score", 95.5);
-
- setupFlagsConfig(true, initialContext);
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture and verify request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // Verify complex nested structures are preserved
- JSONObject locationInRequest = requestContext.getJSONObject("location");
- assertEquals("city should be preserved", "San Francisco", locationInRequest.getString("city"));
- assertEquals("country should be preserved", "USA", locationInRequest.getString("country"));
-
- JSONArray featuresInRequest = requestContext.getJSONArray("features_enabled");
- assertEquals("features array length should be preserved", 2, featuresInRequest.length());
- assertEquals("feature1 should be preserved", "feature1", featuresInRequest.getString(0));
- assertEquals("feature2 should be preserved", "feature2", featuresInRequest.getString(1));
-
- assertTrue("is_beta should be preserved", requestContext.getBoolean("is_beta"));
- assertEquals("score should be preserved", 95.5, requestContext.getDouble("score"), 0.001);
-
- // And distinct_id should still be added
- assertTrue("distinct_id should be added", requestContext.has("distinct_id"));
+ // Wait for all threads to complete
+ assertTrue(
+ "All threads should complete within timeout",
+ completionLatch.await(3000, TimeUnit.MILLISECONDS));
+
+ // Verify results
+ assertEquals("All threads should have completed successfully", threadCount, successCount.get());
+
+ // Only one network request should have been made
+ assertEquals("Should only make one network request", 1, requestCount.get());
+
+ // Verify flags are now ready
+ assertTrue(
+ "Flags should be ready after all getVariant calls complete",
+ mFeatureFlagManager.areFlagsReady());
+
+ // Verify all threads got the correct values (not fallbacks)
+ for (int i = 0; i < threadCount; i++) {
+ MixpanelFlagVariant result = results.get(i);
+ assertNotNull("Thread " + i + " should have a result", result);
+
+ int flagIndex = (i % 3) + 1;
+ String expectedKey = "variant" + flagIndex;
+ String expectedValue = "value" + flagIndex;
+
+ assertEquals("Thread " + i + " should have correct variant key", expectedKey, result.key);
+ assertEquals(
+ "Thread " + i + " should have correct variant value", expectedValue, result.value);
+
+ // Verify it's not the fallback
+ assertNotEquals("Thread " + i + " should not have fallback key", "fallback" + i, result.key);
}
-
- @Test
- public void testFlagsConfigContextUsage_specialCharactersInContext() throws InterruptedException, JSONException {
- // Test that special characters and unicode in context are handled properly
- JSONObject initialContext = new JSONObject();
- initialContext.put("emoji", "🚀🎉");
- initialContext.put("special_chars", "!@#$%^&*()_+-=[]{}|;':\",./<>?");
- initialContext.put("unicode", "ä½ å¥½ä¸–ç•Œ");
- initialContext.put("newline", "line1\nline2");
-
- setupFlagsConfig(true, initialContext);
-
- // Create response
- Map serverFlags = new HashMap<>();
- serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
- mMockRemoteService.addResponse(createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
-
- // Trigger request
- mFeatureFlagManager.loadFlags();
-
- // Capture and verify request
- CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
- JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
- JSONObject requestContext = requestBody.getJSONObject("context");
-
- // Verify special characters are preserved correctly
- assertEquals("emoji should be preserved", "🚀🎉", requestContext.getString("emoji"));
- assertEquals("special_chars should be preserved",
- "!@#$%^&*()_+-=[]{}|;':\",./<>?", requestContext.getString("special_chars"));
- assertEquals("unicode should be preserved", "ä½ å¥½ä¸–ç•Œ", requestContext.getString("unicode"));
- assertEquals("newline should be preserved", "line1\nline2", requestContext.getString("newline"));
+ }
+
+ @Test
+ public void testRequestBodyConstruction_performFetchRequest()
+ throws InterruptedException, JSONException {
+ // Setup with flags enabled and specific context data
+ JSONObject contextData = new JSONObject();
+ contextData.put("$os", "Android");
+ contextData.put("$os_version", "13");
+ contextData.put("custom_property", "test_value");
+ setupFlagsConfig(true, contextData);
+
+ // Set distinct ID for the request
+ mMockDelegate.distinctIdToReturn = "test_user_123";
+
+ // Create response data
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("test_flag", new MixpanelFlagVariant("variant_a", "value_a"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger loadFlags to initiate the request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture the request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ assertNotNull("Request should have been made", capturedRequest);
+
+ // Verify the endpoint URL
+ assertTrue(
+ "URL should contain /flags endpoint", capturedRequest.endpointUrl.contains("/flags"));
+
+ // Parse and verify the request body
+ assertNotNull("Request should have body", capturedRequest.requestBodyBytes);
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+ assertNotNull("Request body should be valid JSON", requestBody);
+
+ // Log the actual request body for debugging
+ MPLog.v("FeatureFlagManagerTest", "Request body: " + requestBody.toString());
+
+ // Verify context is included
+ assertTrue("Request should contain context", requestBody.has("context"));
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // Verify distinct_id is in the context
+ assertTrue("Context should contain distinct_id", requestContext.has("distinct_id"));
+ assertEquals(
+ "Context should contain correct distinct_id",
+ "test_user_123",
+ requestContext.getString("distinct_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"));
+ assertEquals(
+ "Context should contain custom_property",
+ "test_value",
+ requestContext.getString("custom_property"));
+
+ // Verify headers
+ assertNotNull("Request should have headers", capturedRequest.headers);
+ assertEquals(
+ "Content-Type should be application/json with charset",
+ "application/json; charset=utf-8",
+ capturedRequest.headers.get("Content-Type"));
+
+ // Wait for flags to be ready
+ for (int i = 0; i < 20 && !mFeatureFlagManager.areFlagsReady(); i++) {
+ Thread.sleep(100);
}
-}
\ No newline at end of file
+ assertTrue("Flags should be ready", mFeatureFlagManager.areFlagsReady());
+ }
+
+ @Test
+ public void testRequestBodyConstruction_withNullContext()
+ throws InterruptedException, JSONException {
+ // Setup with flags enabled but null context
+ setupFlagsConfig(true, null);
+
+ // Set distinct ID
+ mMockDelegate.distinctIdToReturn = "user_456";
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ assertNotNull("Request should have been made", capturedRequest);
+
+ // Parse request body
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+
+ // Verify context exists
+ assertTrue("Request should contain context", requestBody.has("context"));
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // 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());
+ }
+
+ @Test
+ public void testRequestBodyConstruction_withEmptyDistinctId()
+ throws InterruptedException, JSONException {
+ // Setup with flags enabled
+ setupFlagsConfig(true, new JSONObject());
+
+ // Set empty distinct ID
+ mMockDelegate.distinctIdToReturn = "";
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ assertNotNull("Request should have been made", capturedRequest);
+
+ // Parse request body
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+
+ // Verify context exists
+ assertTrue("Request should contain context", requestBody.has("context"));
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // 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"));
+ }
+
+ @Test
+ public void testFlagsConfigContextUsage_initialContext()
+ throws InterruptedException, JSONException {
+ // Test that initial context from FlagsConfig is properly used
+ JSONObject initialContext = new JSONObject();
+ initialContext.put("app_version", "1.0.0");
+ initialContext.put("platform", "Android");
+ initialContext.put("custom_prop", "initial_value");
+
+ setupFlagsConfig(true, initialContext);
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("test_flag", new MixpanelFlagVariant("v1", "value1"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture and verify request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ assertNotNull("Request should have been made", capturedRequest);
+
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // Verify all initial context properties are included
+ assertEquals(
+ "app_version should be preserved", "1.0.0", requestContext.getString("app_version"));
+ assertEquals("platform should be preserved", "Android", requestContext.getString("platform"));
+ assertEquals(
+ "custom_prop should be preserved",
+ "initial_value",
+ requestContext.getString("custom_prop"));
+
+ // Verify distinct_id is added to context
+ assertTrue("distinct_id should be added to context", requestContext.has("distinct_id"));
+ }
+
+ @Test
+ public void testFlagsConfigContextUsage_contextMerging()
+ throws InterruptedException, JSONException {
+ // Test that distinct_id doesn't override existing context properties
+ JSONObject initialContext = new JSONObject();
+ initialContext.put("distinct_id", "should_be_overridden"); // This should be overridden
+ initialContext.put("user_type", "premium");
+ initialContext.put("$os", "Android");
+
+ setupFlagsConfig(true, initialContext);
+
+ // Set a different distinct_id via delegate
+ mMockDelegate.distinctIdToReturn = "actual_distinct_id";
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture and verify request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // Verify distinct_id from delegate overrides the one in initial context
+ assertEquals(
+ "distinct_id should be from delegate, not initial context",
+ "actual_distinct_id",
+ requestContext.getString("distinct_id"));
+
+ // Verify other properties are preserved
+ assertEquals("user_type should be preserved", "premium", requestContext.getString("user_type"));
+ assertEquals("$os should be preserved", "Android", requestContext.getString("$os"));
+ }
+
+ @Test
+ public void testFlagsConfigContextUsage_emptyContext()
+ throws InterruptedException, JSONException {
+ // Test behavior with empty context object
+ JSONObject emptyContext = new JSONObject();
+ setupFlagsConfig(true, emptyContext);
+
+ mMockDelegate.distinctIdToReturn = "test_user";
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture and verify request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ 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());
+ assertEquals(
+ "distinct_id should be present", "test_user", requestContext.getString("distinct_id"));
+ }
+
+ @Test
+ public void testFlagsConfigContextUsage_complexNestedContext()
+ throws InterruptedException, JSONException {
+ // Test that complex nested objects in context are preserved
+ JSONObject nestedObj = new JSONObject();
+ nestedObj.put("city", "San Francisco");
+ nestedObj.put("country", "USA");
+
+ JSONObject initialContext = new JSONObject();
+ initialContext.put("location", nestedObj);
+ initialContext.put("features_enabled", new JSONArray().put("feature1").put("feature2"));
+ initialContext.put("is_beta", true);
+ initialContext.put("score", 95.5);
+
+ setupFlagsConfig(true, initialContext);
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture and verify request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // Verify complex nested structures are preserved
+ JSONObject locationInRequest = requestContext.getJSONObject("location");
+ assertEquals("city should be preserved", "San Francisco", locationInRequest.getString("city"));
+ assertEquals("country should be preserved", "USA", locationInRequest.getString("country"));
+
+ JSONArray featuresInRequest = requestContext.getJSONArray("features_enabled");
+ assertEquals("features array length should be preserved", 2, featuresInRequest.length());
+ assertEquals("feature1 should be preserved", "feature1", featuresInRequest.getString(0));
+ assertEquals("feature2 should be preserved", "feature2", featuresInRequest.getString(1));
+
+ assertTrue("is_beta should be preserved", requestContext.getBoolean("is_beta"));
+ assertEquals("score should be preserved", 95.5, requestContext.getDouble("score"), 0.001);
+
+ // And distinct_id should still be added
+ assertTrue("distinct_id should be added", requestContext.has("distinct_id"));
+ }
+
+ @Test
+ public void testFlagsConfigContextUsage_specialCharactersInContext()
+ throws InterruptedException, JSONException {
+ // Test that special characters and unicode in context are handled properly
+ JSONObject initialContext = new JSONObject();
+ initialContext.put("emoji", "🚀🎉");
+ initialContext.put("special_chars", "!@#$%^&*()_+-=[]{}|;':\",./<>?");
+ initialContext.put("unicode", "ä½ å¥½ä¸–ç•Œ");
+ initialContext.put("newline", "line1\nline2");
+
+ setupFlagsConfig(true, initialContext);
+
+ // Create response
+ Map serverFlags = new HashMap<>();
+ serverFlags.put("flag", new MixpanelFlagVariant("v", "val"));
+ mMockRemoteService.addResponse(
+ createFlagsResponseJson(serverFlags).getBytes(StandardCharsets.UTF_8));
+
+ // Trigger request
+ mFeatureFlagManager.loadFlags();
+
+ // Capture and verify request
+ CapturedRequest capturedRequest = mMockRemoteService.takeRequest(1000, TimeUnit.MILLISECONDS);
+ JSONObject requestBody = capturedRequest.getRequestBodyAsJson();
+ JSONObject requestContext = requestBody.getJSONObject("context");
+
+ // Verify special characters are preserved correctly
+ assertEquals("emoji should be preserved", "🚀🎉", requestContext.getString("emoji"));
+ assertEquals(
+ "special_chars should be preserved",
+ "!@#$%^&*()_+-=[]{}|;':\",./<>?",
+ requestContext.getString("special_chars"));
+ assertEquals("unicode should be preserved", "ä½ å¥½ä¸–ç•Œ", requestContext.getString("unicode"));
+ assertEquals(
+ "newline should be preserved", "line1\nline2", requestContext.getString("newline"));
+ }
+}
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/HttpTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/HttpTest.java
index e18817196..6df0f7dd0 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/HttpTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/HttpTest.java
@@ -1,23 +1,19 @@
package com.mixpanel.android.mpmetrics;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-
import com.mixpanel.android.util.Base64Coder;
+import com.mixpanel.android.util.HttpService;
import com.mixpanel.android.util.ProxyServerInteractor;
import com.mixpanel.android.util.RemoteService;
-import com.mixpanel.android.util.HttpService;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
@@ -28,303 +24,321 @@
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
-
import javax.net.ssl.SSLSocketFactory;
-
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
@RunWith(AndroidJUnit4.class)
public class HttpTest {
- private Future mMockPreferences;
- private List mFlushResults;
- private BlockingQueue mPerformRequestCalls;
- private List mCleanupCalls;
- private MixpanelAPI mMetrics;
- private volatile int mFlushInterval;
- private volatile boolean mForceOverMemThreshold;
- private static final long POLL_WAIT_MAX_MILLISECONDS = 3500;
- private static final TimeUnit DEFAULT_TIMEUNIT = TimeUnit.MILLISECONDS;
- private static final String SUCCEED_TEXT = "Should Succeed";
- private static final String FAIL_TEXT = "Should Fail";
-
- @Before
- public void setUp() {
- mFlushInterval = 2 * 1000;
- mMockPreferences = new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
- mFlushResults = new ArrayList();
- mPerformRequestCalls = new LinkedBlockingQueue();
- mCleanupCalls = new ArrayList();
- mForceOverMemThreshold = false;
-
- final RemoteService mockPoster = new HttpService() {
- @Override
- public byte[] performRequest(
- @NonNull String endpointUrl,
- @Nullable ProxyServerInteractor interactor,
- @Nullable Map params, // Used only if requestBodyBytes is null
- @Nullable Map headers,
- @Nullable byte[] requestBodyBytes, // If provided, send this as raw body
- @Nullable SSLSocketFactory socketFactory)
- throws ServiceUnavailableException, IOException
- {
- try {
- if (mFlushResults.isEmpty()) {
- mFlushResults.add(TestUtils.bytes("1\n"));
- }
- assertTrue(params.containsKey("data"));
-
- final Object obj = mFlushResults.remove(0);
- if (obj instanceof IOException) {
- throw (IOException)obj;
- } else if (obj instanceof MalformedURLException) {
- throw (MalformedURLException)obj;
- } else if (obj instanceof ServiceUnavailableException) {
- throw (ServiceUnavailableException)obj;
- } else if (obj instanceof SocketTimeoutException) {
- throw (SocketTimeoutException)obj;
- }
-
- final String jsonData = Base64Coder.decodeString(params.get("data").toString());
- JSONArray msg = new JSONArray(jsonData);
- JSONObject event = msg.getJSONObject(0);
- mPerformRequestCalls.put(event.getString("event"));
-
- return (byte[])obj;
- } catch (JSONException e) {
- throw new RuntimeException("Malformed data passed to test mock", e);
- } catch (InterruptedException e) {
- throw new RuntimeException("Could not write message to reporting queue for tests.", e);
- }
+ private Future mMockPreferences;
+ private List mFlushResults;
+ private BlockingQueue mPerformRequestCalls;
+ private List mCleanupCalls;
+ private MixpanelAPI mMetrics;
+ private volatile int mFlushInterval;
+ private volatile boolean mForceOverMemThreshold;
+ private static final long POLL_WAIT_MAX_MILLISECONDS = 3500;
+ private static final TimeUnit DEFAULT_TIMEUNIT = TimeUnit.MILLISECONDS;
+ private static final String SUCCEED_TEXT = "Should Succeed";
+ private static final String FAIL_TEXT = "Should Fail";
+
+ @Before
+ public void setUp() {
+ mFlushInterval = 2 * 1000;
+ mMockPreferences =
+ new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
+ mFlushResults = new ArrayList();
+ mPerformRequestCalls = new LinkedBlockingQueue();
+ mCleanupCalls = new ArrayList();
+ mForceOverMemThreshold = false;
+
+ final RemoteService mockPoster =
+ new HttpService() {
+ @Override
+ public byte[] performRequest(
+ @NonNull String endpointUrl,
+ @Nullable ProxyServerInteractor interactor,
+ @Nullable Map params, // Used only if requestBodyBytes is null
+ @Nullable Map headers,
+ @Nullable byte[] requestBodyBytes, // If provided, send this as raw body
+ @Nullable SSLSocketFactory socketFactory)
+ throws ServiceUnavailableException, IOException {
+ try {
+ if (mFlushResults.isEmpty()) {
+ mFlushResults.add(TestUtils.bytes("1\n"));
+ }
+ assertTrue(params.containsKey("data"));
+
+ final Object obj = mFlushResults.remove(0);
+ if (obj instanceof IOException) {
+ throw (IOException) obj;
+ } else if (obj instanceof MalformedURLException) {
+ throw (MalformedURLException) obj;
+ } else if (obj instanceof ServiceUnavailableException) {
+ throw (ServiceUnavailableException) obj;
+ } else if (obj instanceof SocketTimeoutException) {
+ throw (SocketTimeoutException) obj;
+ }
+
+ final String jsonData = Base64Coder.decodeString(params.get("data").toString());
+ JSONArray msg = new JSONArray(jsonData);
+ JSONObject event = msg.getJSONObject(0);
+ mPerformRequestCalls.put(event.getString("event"));
+
+ return (byte[]) obj;
+ } catch (JSONException e) {
+ throw new RuntimeException("Malformed data passed to test mock", e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(
+ "Could not write message to reporting queue for tests.", e);
}
+ }
};
- final MPConfig config = new MPConfig(new Bundle(), InstrumentationRegistry.getInstrumentation().getContext(), null) {
+ final MPConfig config =
+ new MPConfig(
+ new Bundle(), InstrumentationRegistry.getInstrumentation().getContext(), null) {
- @Override
- public String getEventsEndpoint() {
- return "EVENTS ENDPOINT";
- }
+ @Override
+ public String getEventsEndpoint() {
+ return "EVENTS ENDPOINT";
+ }
- @Override
- public int getFlushInterval() {
- return mFlushInterval;
- }
+ @Override
+ public int getFlushInterval() {
+ return mFlushInterval;
+ }
};
- final MPDbAdapter mockAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), config) {
- @Override
- public void cleanupEvents(String last_id, Table table, String token) {
- mCleanupCalls.add("called");
- super.cleanupEvents(last_id, table, token);
- }
-
- @Override
- protected boolean aboveMemThreshold() {
- if (mForceOverMemThreshold) {
- return true;
- } else {
- return super.aboveMemThreshold();
- }
+ final MPDbAdapter mockAdapter =
+ new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), config) {
+ @Override
+ public void cleanupEvents(String last_id, Table table, String token) {
+ mCleanupCalls.add("called");
+ super.cleanupEvents(last_id, table, token);
+ }
+
+ @Override
+ protected boolean aboveMemThreshold() {
+ if (mForceOverMemThreshold) {
+ return true;
+ } else {
+ return super.aboveMemThreshold();
}
+ }
};
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), config) {
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mockAdapter;
- }
-
- @Override
- protected RemoteService getPoster() {
- return mockPoster;
- }
-
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), config) {
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mockAdapter;
+ }
+
+ @Override
+ protected RemoteService getPoster() {
+ return mockPoster;
+ }
};
- mMetrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Test Message Queuing") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ mMetrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Test Message Queuing") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
};
+ }
+
+ @Test
+ public void testHTTPFailures() {
+ try {
+ runBasicSucceed();
+ runIOException();
+ runMalformedURLException();
+ runServiceUnavailableException(null);
+ runServiceUnavailableException("10");
+ runServiceUnavailableException("40");
+ runDoubleServiceUnavailableException();
+ runBasicSucceed();
+ runMemoryTest();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Test was interrupted.");
}
+ }
- @Test
- public void testHTTPFailures() {
- try {
- runBasicSucceed();
- runIOException();
- runMalformedURLException();
- runServiceUnavailableException(null);
- runServiceUnavailableException("10");
- runServiceUnavailableException("40");
- runDoubleServiceUnavailableException();
- runBasicSucceed();
- runMemoryTest();
- } catch (InterruptedException e) {
- throw new RuntimeException("Test was interrupted.");
- }
- }
-
- public void runBasicSucceed() throws InterruptedException {
- mCleanupCalls.clear();
- mMetrics.track(SUCCEED_TEXT, null);
- waitForFlushInternval();
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll());
- assertEquals(1, mCleanupCalls.size());
- }
-
- public void runIOException() throws InterruptedException {
- mCleanupCalls.clear();
- mFlushResults.add(new IOException());
- mMetrics.track(SUCCEED_TEXT, null);
+ public void runBasicSucceed() throws InterruptedException {
+ mCleanupCalls.clear();
+ mMetrics.track(SUCCEED_TEXT, null);
+ waitForFlushInternval();
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll());
+ assertEquals(1, mCleanupCalls.size());
+ }
- waitForFlushInternval();
+ public void runIOException() throws InterruptedException {
+ mCleanupCalls.clear();
+ mFlushResults.add(new IOException());
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(0, mCleanupCalls.size());
+ waitForFlushInternval();
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(0, mCleanupCalls.size());
- waitForBackOffTimeInterval();
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(1, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ waitForBackOffTimeInterval();
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(1, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- waitForFlushInternval();
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(2, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- }
-
- public void runMalformedURLException() throws InterruptedException {
- mCleanupCalls.clear();
- mFlushResults.add(new MalformedURLException());
- mMetrics.track(SUCCEED_TEXT, null);
-
- waitForFlushInternval();
+ waitForFlushInternval();
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(1, mCleanupCalls.size());
+ assertEquals(2, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ }
- mFlushResults.add(new MalformedURLException());
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ public void runMalformedURLException() throws InterruptedException {
+ mCleanupCalls.clear();
+ mFlushResults.add(new MalformedURLException());
+ mMetrics.track(SUCCEED_TEXT, null);
- waitForFlushInternval();
+ waitForFlushInternval();
- assertEquals(2, mCleanupCalls.size());
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(1, mCleanupCalls.size());
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ mFlushResults.add(new MalformedURLException());
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- waitForFlushInternval();
+ waitForFlushInternval();
- assertEquals(3, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- }
-
- private void runServiceUnavailableException(String retryAfterSeconds) throws InterruptedException {
- mCleanupCalls.clear();
- mFlushResults.add(new RemoteService.ServiceUnavailableException("", retryAfterSeconds));
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(2, mCleanupCalls.size());
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- waitForFlushInternval();
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(0, mCleanupCalls.size());
+ waitForFlushInternval();
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(3, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ }
- waitForBackOffTimeInterval();
+ private void runServiceUnavailableException(String retryAfterSeconds)
+ throws InterruptedException {
+ mCleanupCalls.clear();
+ mFlushResults.add(new RemoteService.ServiceUnavailableException("", retryAfterSeconds));
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(1, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ waitForFlushInternval();
- mMetrics.track(SUCCEED_TEXT, null);
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(0, mCleanupCalls.size());
- waitForFlushInternval();
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(2, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- }
+ waitForBackOffTimeInterval();
- private void runDoubleServiceUnavailableException() throws InterruptedException {
- mCleanupCalls.clear();
- mFlushResults.add(new RemoteService.ServiceUnavailableException("", ""));
- mFlushResults.add(new RemoteService.ServiceUnavailableException("", ""));
- mMetrics.track(SUCCEED_TEXT, null);
+ assertEquals(1, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- waitForFlushInternval();
+ mMetrics.track(SUCCEED_TEXT, null);
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(0, mCleanupCalls.size());
+ waitForFlushInternval();
- int numEvents = 2 * 50 + 20; // we send batches of 50 each time
- for (int i = 0; i <= numEvents; i++) {
- mMetrics.track(SUCCEED_TEXT, null);
- }
+ assertEquals(2, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ }
- waitForBackOffTimeInterval();
+ private void runDoubleServiceUnavailableException() throws InterruptedException {
+ mCleanupCalls.clear();
+ mFlushResults.add(new RemoteService.ServiceUnavailableException("", ""));
+ mFlushResults.add(new RemoteService.ServiceUnavailableException("", ""));
+ mMetrics.track(SUCCEED_TEXT, null);
- assertEquals(0, mCleanupCalls.size());
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ waitForFlushInternval();
- waitForBackOffTimeInterval();
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(0, mCleanupCalls.size());
- assertEquals(3, mCleanupCalls.size());
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ int numEvents = 2 * 50 + 20; // we send batches of 50 each time
+ for (int i = 0; i <= numEvents; i++) {
+ mMetrics.track(SUCCEED_TEXT, null);
}
- private void runMemoryTest() throws InterruptedException {
- mForceOverMemThreshold = true;
- mCleanupCalls.clear();
- mMetrics.track(FAIL_TEXT, null);
- waitForFlushInternval();
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(0, mCleanupCalls.size());
-
- mForceOverMemThreshold = false;
- mMetrics.track(SUCCEED_TEXT, null);
- waitForFlushInternval();
- assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
- assertEquals(1, mCleanupCalls.size());
- }
-
- private void waitForBackOffTimeInterval() throws InterruptedException {
- long waitForMs = mMetrics.getAnalyticsMessages().getTrackEngageRetryAfter();
- Thread.sleep(waitForMs);
- Thread.sleep(1500);
- }
-
- private void waitForFlushInternval() throws InterruptedException {
- Thread.sleep(mFlushInterval);
- Thread.sleep(1500);
- }
+ waitForBackOffTimeInterval();
+
+ assertEquals(0, mCleanupCalls.size());
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+
+ waitForBackOffTimeInterval();
+
+ assertEquals(3, mCleanupCalls.size());
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ }
+
+ private void runMemoryTest() throws InterruptedException {
+ mForceOverMemThreshold = true;
+ mCleanupCalls.clear();
+ mMetrics.track(FAIL_TEXT, null);
+ waitForFlushInternval();
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(0, mCleanupCalls.size());
+
+ mForceOverMemThreshold = false;
+ mMetrics.track(SUCCEED_TEXT, null);
+ waitForFlushInternval();
+ assertEquals(
+ SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT));
+ assertEquals(1, mCleanupCalls.size());
+ }
+
+ private void waitForBackOffTimeInterval() throws InterruptedException {
+ long waitForMs = mMetrics.getAnalyticsMessages().getTrackEngageRetryAfter();
+ Thread.sleep(waitForMs);
+ Thread.sleep(1500);
+ }
+
+ private void waitForFlushInternval() throws InterruptedException {
+ Thread.sleep(mFlushInterval);
+ Thread.sleep(1500);
+ }
}
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/MPConfigTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/MPConfigTest.java
index 1852a8ad5..e1743fbcd 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/MPConfigTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/MPConfigTest.java
@@ -1,231 +1,253 @@
package com.mixpanel.android.mpmetrics;
-import android.os.Bundle;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.os.Bundle;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
import java.util.UUID;
+import org.junit.Test;
+import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class MPConfigTest {
- public static final String TOKEN = "TOKEN";
- public static final String DISABLE_VIEW_CRAWLER_METADATA_KEY = "com.mixpanel.android.MPConfig.DisableViewCrawler";
-
- @Test
- public void testSetUseIpAddressForGeolocation() {
- final Bundle metaData = new Bundle();
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
-
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
-
- mixpanelAPI.setUseIpAddressForGeolocation(false);
- assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
-
- mixpanelAPI.setUseIpAddressForGeolocation(true);
- assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
- }
-
- @Test
- public void testSetUseIpAddressForGeolocationOverwrite() {
- final Bundle metaData = new Bundle();
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
- metaData.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=1");
-
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
-
- mixpanelAPI.setUseIpAddressForGeolocation(false);
- assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
-
- final Bundle metaData2 = new Bundle();
- metaData2.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=0");
- metaData2.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=0");
-
- MPConfig config2 = mpConfig(metaData2);
- final MixpanelAPI mixpanelAPI2 = mixpanelApi(config2);
- assertEquals("https://api.mixpanel.com/track/?ip=0", config2.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=0", config2.getPeopleEndpoint());
-
- mixpanelAPI2.setUseIpAddressForGeolocation(true);
- assertEquals("https://api.mixpanel.com/track/?ip=1", config2.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=1", config2.getPeopleEndpoint());
- }
-
- @Test
- public void testEndPointAndGeoSettingBothReadFromConfigTrue() {
- final Bundle metaData = new Bundle();
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/");
- metaData.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/");
- metaData.putString("com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/");
- metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", true);
-
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
- }
-
- public void testSetServerURL() throws Exception {
- final Bundle metaData = new Bundle();
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- // default Mixpanel endpoint
- assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
-
- mixpanelAPI.setServerURL("https://api-eu.mixpanel.com");
- assertEquals("https://api-eu.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
- assertEquals("https://api-eu.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
- assertEquals("https://api-eu.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
- }
-
- @Test
- public void testEndPointAndGeoSettingBothReadFromConfigFalse() {
- final Bundle metaData = new Bundle();
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/");
- metaData.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/");
- metaData.putString("com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/");
- metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", false);
-
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
- }
-
- @Test
- public void testEndPointAndGeoSettingBothReadFromConfigFalseOverwrite() {
- final Bundle metaData = new Bundle();
- metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
- metaData.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=1");
- metaData.putString("com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/?ip=1");
- metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", false);
-
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
- assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
- assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
- }
-
- @Test
- public void testSetEnableLogging() throws Exception {
- final Bundle metaData = new Bundle();
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- mixpanelAPI.setEnableLogging(true);
- assertTrue(config.DEBUG);
- mixpanelAPI.setEnableLogging(false);
- assertFalse(config.DEBUG);
- }
-
-
- @Test
- public void testSetFlushBatchSize() {
- final Bundle metaData = new Bundle();
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- mixpanelAPI.setFlushBatchSize(10);
- assertEquals(10, config.getFlushBatchSize());
- mixpanelAPI.setFlushBatchSize(100);
- assertEquals(100, config.getFlushBatchSize());
- }
-
- @Test
- public void testSetFlushBatchSize2() {
- final Bundle metaData = new Bundle();
- metaData.putInt("com.mixpanel.android.MPConfig.FlushBatchSize", 5);
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals(5, mixpanelAPI.getFlushBatchSize());
- }
-
- @Test
- public void testSetFlushBatchSizeMulptipleConfigs() {
- String fakeToken = UUID.randomUUID().toString();
- MixpanelAPI mixpanel1 = MixpanelAPI.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), fakeToken, false);
- mixpanel1.setFlushBatchSize(10);
- assertEquals(10, mixpanel1.getFlushBatchSize());
-
- String fakeToken2 = UUID.randomUUID().toString();
- MixpanelAPI mixpanel2 = MixpanelAPI.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), fakeToken2, false);
- mixpanel2.setFlushBatchSize(20);
- assertEquals(20, mixpanel2.getFlushBatchSize());
- // mixpanel2 should not overwrite the settings to mixpanel1
- assertEquals(10, mixpanel1.getFlushBatchSize());
- }
-
-
- @Test
- public void testSetMaximumDatabaseLimit() {
- final Bundle metaData = new Bundle();
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- mixpanelAPI.setMaximumDatabaseLimit(10000);
- assertEquals(10000, config.getMaximumDatabaseLimit());
- }
-
- @Test
- public void testSetMaximumDatabaseLimit2() {
- final Bundle metaData = new Bundle();
- metaData.putInt("com.mixpanel.android.MPConfig.MaximumDatabaseLimit", 100000000);
- MPConfig config = mpConfig(metaData);
- final MixpanelAPI mixpanelAPI = mixpanelApi(config);
- assertEquals(100000000, mixpanelAPI.getMaximumDatabaseLimit());
- }
-
- @Test
- public void testShouldGzipRequestPayload() {
- final Bundle metaData = new Bundle();
- metaData.putBoolean("com.mixpanel.android.MPConfig.GzipRequestPayload", true);
- MPConfig mpConfig = mpConfig(metaData);
- assertTrue(mpConfig.shouldGzipRequestPayload());
-
- mpConfig.setShouldGzipRequestPayload(false);
- assertFalse(mpConfig.shouldGzipRequestPayload());
-
- mpConfig.setShouldGzipRequestPayload(true);
- assertTrue(mpConfig.shouldGzipRequestPayload());
-
- // assert false by default
- MPConfig mpConfig2 = mpConfig(new Bundle());
- assertFalse(mpConfig2.shouldGzipRequestPayload());
-
- MixpanelAPI mixpanelAPI = mixpanelApi(mpConfig);
-
- assertTrue(mixpanelAPI.shouldGzipRequestPayload());
-
- mixpanelAPI.setShouldGzipRequestPayload(false);
- assertFalse(mixpanelAPI.shouldGzipRequestPayload());
-
- }
-
- private MPConfig mpConfig(final Bundle metaData) {
- return new MPConfig(metaData, InstrumentationRegistry.getInstrumentation().getContext(), null);
- }
-
- private MixpanelAPI mixpanelApi(final MPConfig config) {
- return new MixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext()), TOKEN, config, false, null,null, true);
- }
+ public static final String TOKEN = "TOKEN";
+ public static final String DISABLE_VIEW_CRAWLER_METADATA_KEY =
+ "com.mixpanel.android.MPConfig.DisableViewCrawler";
+
+ @Test
+ public void testSetUseIpAddressForGeolocation() {
+ final Bundle metaData = new Bundle();
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
+
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+
+ mixpanelAPI.setUseIpAddressForGeolocation(false);
+ assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
+
+ mixpanelAPI.setUseIpAddressForGeolocation(true);
+ assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
+ }
+
+ @Test
+ public void testSetUseIpAddressForGeolocationOverwrite() {
+ final Bundle metaData = new Bundle();
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=1");
+
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
+
+ mixpanelAPI.setUseIpAddressForGeolocation(false);
+ assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
+
+ final Bundle metaData2 = new Bundle();
+ metaData2.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=0");
+ metaData2.putString(
+ "com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=0");
+
+ MPConfig config2 = mpConfig(metaData2);
+ final MixpanelAPI mixpanelAPI2 = mixpanelApi(config2);
+ assertEquals("https://api.mixpanel.com/track/?ip=0", config2.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=0", config2.getPeopleEndpoint());
+
+ mixpanelAPI2.setUseIpAddressForGeolocation(true);
+ assertEquals("https://api.mixpanel.com/track/?ip=1", config2.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=1", config2.getPeopleEndpoint());
+ }
+
+ @Test
+ public void testEndPointAndGeoSettingBothReadFromConfigTrue() {
+ final Bundle metaData = new Bundle();
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/");
+ metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", true);
+
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
+ }
+
+ public void testSetServerURL() throws Exception {
+ final Bundle metaData = new Bundle();
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ // default Mixpanel endpoint
+ assertEquals("https://api.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
+
+ mixpanelAPI.setServerURL("https://api-eu.mixpanel.com");
+ assertEquals("https://api-eu.mixpanel.com/track/?ip=1", config.getEventsEndpoint());
+ assertEquals("https://api-eu.mixpanel.com/engage/?ip=1", config.getPeopleEndpoint());
+ assertEquals("https://api-eu.mixpanel.com/groups/?ip=1", config.getGroupsEndpoint());
+ }
+
+ @Test
+ public void testEndPointAndGeoSettingBothReadFromConfigFalse() {
+ final Bundle metaData = new Bundle();
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/");
+ metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", false);
+
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
+ }
+
+ @Test
+ public void testEndPointAndGeoSettingBothReadFromConfigFalseOverwrite() {
+ final Bundle metaData = new Bundle();
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.EventsEndpoint", "https://api.mixpanel.com/track/?ip=1");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.PeopleEndpoint", "https://api.mixpanel.com/engage/?ip=1");
+ metaData.putString(
+ "com.mixpanel.android.MPConfig.GroupsEndpoint", "https://api.mixpanel.com/groups/?ip=1");
+ metaData.putBoolean("com.mixpanel.android.MPConfig.UseIpAddressForGeolocation", false);
+
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals("https://api.mixpanel.com/track/?ip=0", config.getEventsEndpoint());
+ assertEquals("https://api.mixpanel.com/engage/?ip=0", config.getPeopleEndpoint());
+ assertEquals("https://api.mixpanel.com/groups/?ip=0", config.getGroupsEndpoint());
+ }
+
+ @Test
+ public void testSetEnableLogging() throws Exception {
+ final Bundle metaData = new Bundle();
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ mixpanelAPI.setEnableLogging(true);
+ assertTrue(config.DEBUG);
+ mixpanelAPI.setEnableLogging(false);
+ assertFalse(config.DEBUG);
+ }
+
+ @Test
+ public void testSetFlushBatchSize() {
+ final Bundle metaData = new Bundle();
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ mixpanelAPI.setFlushBatchSize(10);
+ assertEquals(10, config.getFlushBatchSize());
+ mixpanelAPI.setFlushBatchSize(100);
+ assertEquals(100, config.getFlushBatchSize());
+ }
+
+ @Test
+ public void testSetFlushBatchSize2() {
+ final Bundle metaData = new Bundle();
+ metaData.putInt("com.mixpanel.android.MPConfig.FlushBatchSize", 5);
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals(5, mixpanelAPI.getFlushBatchSize());
+ }
+
+ @Test
+ public void testSetFlushBatchSizeMulptipleConfigs() {
+ String fakeToken = UUID.randomUUID().toString();
+ MixpanelAPI mixpanel1 =
+ MixpanelAPI.getInstance(
+ InstrumentationRegistry.getInstrumentation().getContext(), fakeToken, false);
+ mixpanel1.setFlushBatchSize(10);
+ assertEquals(10, mixpanel1.getFlushBatchSize());
+
+ String fakeToken2 = UUID.randomUUID().toString();
+ MixpanelAPI mixpanel2 =
+ MixpanelAPI.getInstance(
+ InstrumentationRegistry.getInstrumentation().getContext(), fakeToken2, false);
+ mixpanel2.setFlushBatchSize(20);
+ assertEquals(20, mixpanel2.getFlushBatchSize());
+ // mixpanel2 should not overwrite the settings to mixpanel1
+ assertEquals(10, mixpanel1.getFlushBatchSize());
+ }
+
+ @Test
+ public void testSetMaximumDatabaseLimit() {
+ final Bundle metaData = new Bundle();
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ mixpanelAPI.setMaximumDatabaseLimit(10000);
+ assertEquals(10000, config.getMaximumDatabaseLimit());
+ }
+
+ @Test
+ public void testSetMaximumDatabaseLimit2() {
+ final Bundle metaData = new Bundle();
+ metaData.putInt("com.mixpanel.android.MPConfig.MaximumDatabaseLimit", 100000000);
+ MPConfig config = mpConfig(metaData);
+ final MixpanelAPI mixpanelAPI = mixpanelApi(config);
+ assertEquals(100000000, mixpanelAPI.getMaximumDatabaseLimit());
+ }
+
+ @Test
+ public void testShouldGzipRequestPayload() {
+ final Bundle metaData = new Bundle();
+ metaData.putBoolean("com.mixpanel.android.MPConfig.GzipRequestPayload", true);
+ MPConfig mpConfig = mpConfig(metaData);
+ assertTrue(mpConfig.shouldGzipRequestPayload());
+
+ mpConfig.setShouldGzipRequestPayload(false);
+ assertFalse(mpConfig.shouldGzipRequestPayload());
+
+ mpConfig.setShouldGzipRequestPayload(true);
+ assertTrue(mpConfig.shouldGzipRequestPayload());
+
+ // assert false by default
+ MPConfig mpConfig2 = mpConfig(new Bundle());
+ assertFalse(mpConfig2.shouldGzipRequestPayload());
+
+ MixpanelAPI mixpanelAPI = mixpanelApi(mpConfig);
+
+ assertTrue(mixpanelAPI.shouldGzipRequestPayload());
+
+ mixpanelAPI.setShouldGzipRequestPayload(false);
+ assertFalse(mixpanelAPI.shouldGzipRequestPayload());
+ }
+
+ private MPConfig mpConfig(final Bundle metaData) {
+ return new MPConfig(metaData, InstrumentationRegistry.getInstrumentation().getContext(), null);
+ }
+
+ private MixpanelAPI mixpanelApi(final MPConfig config) {
+ return new MixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext()),
+ TOKEN,
+ config,
+ false,
+ null,
+ null,
+ true);
+ }
}
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
index 56f64f411..c1ab94c51 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
@@ -1,31 +1,31 @@
package com.mixpanel.android.mpmetrics;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
-
import com.mixpanel.android.BuildConfig;
import com.mixpanel.android.util.Base64Coder;
import com.mixpanel.android.util.HttpService;
import com.mixpanel.android.util.ProxyServerInteractor;
import com.mixpanel.android.util.RemoteService;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import java.io.IOException;
-import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
@@ -36,1714 +36,2094 @@
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
-
import javax.net.ssl.SSLSocketFactory;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.hamcrest.CoreMatchers.*;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MixpanelBasicTest {
- @Before
- public void setUp() throws Exception {
- mMockPreferences = new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
- AnalyticsMessages messages = AnalyticsMessages.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null));
- messages.hardKill();
- Thread.sleep(2000);
-
- try {
- SystemInformation systemInformation = SystemInformation.getInstance(InstrumentationRegistry.getInstrumentation().getContext());
-
- final StringBuilder queryBuilder = new StringBuilder();
- queryBuilder.append("&properties=");
- JSONObject properties = new JSONObject();
- properties.putOpt("$android_lib_version", MPConfig.VERSION);
- properties.putOpt("$android_app_version", systemInformation.getAppVersionName());
- properties.putOpt("$android_version", Build.VERSION.RELEASE);
- properties.putOpt("$android_app_release", systemInformation.getAppVersionCode());
- properties.putOpt("$android_device_model", Build.MODEL);
- queryBuilder.append(URLEncoder.encode(properties.toString(), "utf-8"));
- mAppProperties = queryBuilder.toString();
- } catch (Exception e) {}
- } // end of setUp() method definition
-
- @Test
- public void testVersionsMatch() {
- assertEquals(BuildConfig.MIXPANEL_VERSION, MPConfig.VERSION);
+ @Before
+ public void setUp() throws Exception {
+ mMockPreferences =
+ new TestUtils.EmptyPreferences(InstrumentationRegistry.getInstrumentation().getContext());
+ AnalyticsMessages messages =
+ AnalyticsMessages.getInstance(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null));
+ messages.hardKill();
+ Thread.sleep(2000);
+
+ try {
+ SystemInformation systemInformation =
+ SystemInformation.getInstance(InstrumentationRegistry.getInstrumentation().getContext());
+
+ final StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append("&properties=");
+ JSONObject properties = new JSONObject();
+ properties.putOpt("$android_lib_version", MPConfig.VERSION);
+ properties.putOpt("$android_app_version", systemInformation.getAppVersionName());
+ properties.putOpt("$android_version", Build.VERSION.RELEASE);
+ properties.putOpt("$android_app_release", systemInformation.getAppVersionCode());
+ properties.putOpt("$android_device_model", Build.MODEL);
+ queryBuilder.append(URLEncoder.encode(properties.toString(), "utf-8"));
+ mAppProperties = queryBuilder.toString();
+ } catch (Exception e) {
}
-
- @Test
- public void testGeneratedDistinctId() {
- String fakeToken = UUID.randomUUID().toString();
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, fakeToken);
- String generatedId1 = mixpanel.getDistinctId();
- assertThat(generatedId1, startsWith("$device:"));
- assertEquals(generatedId1, "$device:" + mixpanel.getAnonymousId());
-
- mixpanel.reset();
- String generatedId2 = mixpanel.getDistinctId();
- assertThat(generatedId2, startsWith("$device:"));
- assertEquals(generatedId2, "$device:" + mixpanel.getAnonymousId());
- assertNotEquals(generatedId1, generatedId2);
- }
-
- @Test
- public void testDeleteDB() {
- Map beforeMap = new HashMap();
- beforeMap.put("added", "before");
- JSONObject before = new JSONObject(beforeMap);
-
- Map afterMap = new HashMap();
- afterMap.put("added", "after");
- JSONObject after = new JSONObject(afterMap);
-
- MPDbAdapter adapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), "DeleteTestDB", MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null));
- adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.EVENTS);
- adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.PEOPLE);
- adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.GROUPS);
- adapter.deleteDB();
-
- String[] emptyEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS, "ATOKEN");
- assertNull(emptyEventsData);
- String[] emptyPeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE, "ATOKEN");
- assertNull(emptyPeopleData);
- String[] emptyGroupsData = adapter.generateDataString(MPDbAdapter.Table.GROUPS, "ATOKEN");
- assertNull(emptyGroupsData);
-
- adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.EVENTS);
- adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.PEOPLE);
- adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.GROUPS);
-
- try {
- String[] someEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS, "ATOKEN");
- JSONArray someEvents = new JSONArray(someEventsData[1]);
- assertEquals(someEvents.length(), 1);
- assertEquals(someEvents.getJSONObject(0).get("added"), "after");
-
- String[] somePeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE, "ATOKEN");
- JSONArray somePeople = new JSONArray(somePeopleData[1]);
- assertEquals(somePeople.length(), 1);
- assertEquals(somePeople.getJSONObject(0).get("added"), "after");
-
- String[] someGroupsData = adapter.generateDataString(MPDbAdapter.Table.GROUPS, "ATOKEN");
- JSONArray someGroups = new JSONArray(somePeopleData[1]);
- assertEquals(someGroups.length(), 1);
- assertEquals(someGroups.getJSONObject(0).get("added"), "after"); } catch (JSONException e) {
- fail("Unexpected JSON or lack thereof in MPDbAdapter test");
- }
+ } // end of setUp() method definition
+
+ @Test
+ public void testVersionsMatch() {
+ assertEquals(BuildConfig.MIXPANEL_VERSION, MPConfig.VERSION);
+ }
+
+ @Test
+ public void testGeneratedDistinctId() {
+ String fakeToken = UUID.randomUUID().toString();
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, fakeToken);
+ String generatedId1 = mixpanel.getDistinctId();
+ assertThat(generatedId1, startsWith("$device:"));
+ assertEquals(generatedId1, "$device:" + mixpanel.getAnonymousId());
+
+ mixpanel.reset();
+ String generatedId2 = mixpanel.getDistinctId();
+ assertThat(generatedId2, startsWith("$device:"));
+ assertEquals(generatedId2, "$device:" + mixpanel.getAnonymousId());
+ assertNotEquals(generatedId1, generatedId2);
+ }
+
+ @Test
+ public void testDeleteDB() {
+ Map beforeMap = new HashMap();
+ beforeMap.put("added", "before");
+ JSONObject before = new JSONObject(beforeMap);
+
+ Map afterMap = new HashMap();
+ afterMap.put("added", "after");
+ JSONObject after = new JSONObject(afterMap);
+
+ MPDbAdapter adapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ "DeleteTestDB",
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null));
+ adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.EVENTS);
+ adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.PEOPLE);
+ adapter.addJSON(before, "ATOKEN", MPDbAdapter.Table.GROUPS);
+ adapter.deleteDB();
+
+ String[] emptyEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS, "ATOKEN");
+ assertNull(emptyEventsData);
+ String[] emptyPeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE, "ATOKEN");
+ assertNull(emptyPeopleData);
+ String[] emptyGroupsData = adapter.generateDataString(MPDbAdapter.Table.GROUPS, "ATOKEN");
+ assertNull(emptyGroupsData);
+
+ adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.EVENTS);
+ adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.PEOPLE);
+ adapter.addJSON(after, "ATOKEN", MPDbAdapter.Table.GROUPS);
+
+ try {
+ String[] someEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS, "ATOKEN");
+ JSONArray someEvents = new JSONArray(someEventsData[1]);
+ assertEquals(someEvents.length(), 1);
+ assertEquals(someEvents.getJSONObject(0).get("added"), "after");
+
+ String[] somePeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE, "ATOKEN");
+ JSONArray somePeople = new JSONArray(somePeopleData[1]);
+ assertEquals(somePeople.length(), 1);
+ assertEquals(somePeople.getJSONObject(0).get("added"), "after");
+
+ String[] someGroupsData = adapter.generateDataString(MPDbAdapter.Table.GROUPS, "ATOKEN");
+ JSONArray someGroups = new JSONArray(somePeopleData[1]);
+ assertEquals(someGroups.length(), 1);
+ assertEquals(someGroups.getJSONObject(0).get("added"), "after");
+ } catch (JSONException e) {
+ fail("Unexpected JSON or lack thereof in MPDbAdapter test");
}
-
- @Test
- public void testLooperDestruction() {
- final BlockingQueue messages = new LinkedBlockingQueue();
-
- final MPDbAdapter explodingDb = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
- messages.add(message);
- throw new RuntimeException("BANG!");
- }
+ }
+
+ @Test
+ public void testLooperDestruction() {
+ final BlockingQueue messages = new LinkedBlockingQueue();
+
+ final MPDbAdapter explodingDb =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
+ messages.add(message);
+ throw new RuntimeException("BANG!");
+ }
};
- final AnalyticsMessages explodingMessages = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- // This will throw inside of our worker thread.
- @Override
- public MPDbAdapter makeDbAdapter(Context context) {
- return explodingDb;
- }
+ final AnalyticsMessages explodingMessages =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ // This will throw inside of our worker thread.
+ @Override
+ public MPDbAdapter makeDbAdapter(Context context) {
+ return explodingDb;
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "TEST TOKEN testLooperDisaster") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return explodingMessages;
- }
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "TEST TOKEN testLooperDisaster") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return explodingMessages;
+ }
};
- try {
- mixpanel.reset();
- assertFalse(explodingMessages.isDead());
+ try {
+ mixpanel.reset();
+ assertFalse(explodingMessages.isDead());
- mixpanel.track("event1", null);
- JSONObject found = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertNotNull("should found", found);
+ mixpanel.track("event1", null);
+ JSONObject found = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertNotNull("should found", found);
- Thread.sleep(1000);
- assertTrue(explodingMessages.isDead());
+ Thread.sleep(1000);
+ assertTrue(explodingMessages.isDead());
- mixpanel.track("event2", null);
- JSONObject shouldntFind = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertNull(shouldntFind);
- assertTrue(explodingMessages.isDead());
- } catch (InterruptedException e) {
- fail("Unexpected interruption");
- }
+ mixpanel.track("event2", null);
+ JSONObject shouldntFind = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertNull(shouldntFind);
+ assertTrue(explodingMessages.isDead());
+ } catch (InterruptedException e) {
+ fail("Unexpected interruption");
}
-
- @Test
- public void testEventOperations() throws JSONException {
- final BlockingQueue messages = new LinkedBlockingQueue();
-
- final MPDbAdapter eventOperationsAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
- messages.add(message);
-
- return 1;
- }
+ }
+
+ @Test
+ public void testEventOperations() throws JSONException {
+ final BlockingQueue messages = new LinkedBlockingQueue();
+
+ final MPDbAdapter eventOperationsAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
+ messages.add(message);
+
+ return 1;
+ }
};
- final AnalyticsMessages eventOperationsMessages = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- // This will throw inside of our worker thread.
- @Override
- public MPDbAdapter makeDbAdapter(Context context) {
- return eventOperationsAdapter;
- }
+ final AnalyticsMessages eventOperationsMessages =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ // This will throw inside of our worker thread.
+ @Override
+ public MPDbAdapter makeDbAdapter(Context context) {
+ return eventOperationsAdapter;
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Test event operations") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return eventOperationsMessages;
- }
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Test event operations") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return eventOperationsMessages;
+ }
};
- JSONObject jsonObj1 = new JSONObject();
- JSONObject jsonObj2 = new JSONObject();
- JSONObject jsonObj3 = new JSONObject();
- JSONObject jsonObj4 = new JSONObject();
- JSONObject jsonObj5 = new JSONObject();
-
- Map mapObj1 = new HashMap<>();
- Map mapObj2 = new HashMap<>();
- Map mapObj3 = new HashMap<>();
- Map mapObj4 = new HashMap<>();
- Map mapObj5 = new HashMap<>();
-
- jsonObj1.put("TRACK JSON STRING", "TRACK JSON STRING VALUE");
- jsonObj2.put("TRACK JSON INT", 1);
- jsonObj3.put("TRACK JSON STRING ONCE", "TRACK JSON STRING ONCE VALUE");
- jsonObj4.put("TRACK JSON STRING ONCE", "SHOULD NOT SEE ME");
- jsonObj5.put("TRACK JSON NULL", JSONObject.NULL);
-
-
- mapObj1.put("TRACK MAP STRING", "TRACK MAP STRING VALUE");
- mapObj2.put("TRACK MAP INT", 1);
- mapObj3.put("TRACK MAP STRING ONCE", "TRACK MAP STRING ONCE VALUE");
- mapObj4.put("TRACK MAP STRING ONCE", "SHOULD NOT SEE ME");
- mapObj5.put("TRACK MAP CUSTOM OBJECT", mixpanel);
-
- try {
- JSONObject message;
- JSONObject properties;
-
- mixpanel.track("event1", null);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event1", message.getString("event"));
-
- mixpanel.track("event2", jsonObj1);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event2", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertEquals(jsonObj1.getString("TRACK JSON STRING"), properties.getString("TRACK JSON STRING"));
-
- mixpanel.trackMap("event3", null);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event3", message.getString("event"));
-
- mixpanel.trackMap("event4", mapObj1);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event4", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertEquals(mapObj1.get("TRACK MAP STRING"), properties.getString("TRACK MAP STRING"));
-
- mixpanel.registerSuperProperties(jsonObj2);
- mixpanel.registerSuperPropertiesOnce(jsonObj3);
- mixpanel.registerSuperPropertiesOnce(jsonObj4);
- mixpanel.registerSuperPropertiesMap(mapObj2);
- mixpanel.registerSuperPropertiesOnceMap(mapObj3);
- mixpanel.registerSuperPropertiesOnceMap(mapObj4);
-
- mixpanel.track("event5", null);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event5", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertEquals(jsonObj2.getInt("TRACK JSON INT"), properties.getInt("TRACK JSON INT"));
- assertEquals(jsonObj3.getString("TRACK JSON STRING ONCE"), properties.getString("TRACK JSON STRING ONCE"));
- assertEquals(mapObj2.get("TRACK MAP INT"), properties.getInt("TRACK MAP INT"));
- assertEquals(mapObj3.get("TRACK MAP STRING ONCE"), properties.getString("TRACK MAP STRING ONCE"));
-
- mixpanel.unregisterSuperProperty("TRACK JSON INT");
- mixpanel.track("event6", null);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event6", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertFalse(properties.has("TRACK JSON INT"));
-
- mixpanel.clearSuperProperties();
- mixpanel.track("event7", null);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event7", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertFalse(properties.has("TRACK JSON STRING ONCE"));
-
- mixpanel.track("event8", jsonObj5);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event8", message.getString("event"));
- properties = message.getJSONObject("properties");
- assertEquals(jsonObj5.get("TRACK JSON NULL"), properties.get("TRACK JSON NULL"));
-
- mixpanel.trackMap("event contains custom object", mapObj5);
- message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("event contains custom object", message.getString("event"));
- } catch (InterruptedException e) {
- fail("Unexpected interruption");
- }
+ JSONObject jsonObj1 = new JSONObject();
+ JSONObject jsonObj2 = new JSONObject();
+ JSONObject jsonObj3 = new JSONObject();
+ JSONObject jsonObj4 = new JSONObject();
+ JSONObject jsonObj5 = new JSONObject();
+
+ Map mapObj1 = new HashMap<>();
+ Map mapObj2 = new HashMap<>();
+ Map mapObj3 = new HashMap<>();
+ Map mapObj4 = new HashMap<>();
+ Map mapObj5 = new HashMap<>();
+
+ jsonObj1.put("TRACK JSON STRING", "TRACK JSON STRING VALUE");
+ jsonObj2.put("TRACK JSON INT", 1);
+ jsonObj3.put("TRACK JSON STRING ONCE", "TRACK JSON STRING ONCE VALUE");
+ jsonObj4.put("TRACK JSON STRING ONCE", "SHOULD NOT SEE ME");
+ jsonObj5.put("TRACK JSON NULL", JSONObject.NULL);
+
+ mapObj1.put("TRACK MAP STRING", "TRACK MAP STRING VALUE");
+ mapObj2.put("TRACK MAP INT", 1);
+ mapObj3.put("TRACK MAP STRING ONCE", "TRACK MAP STRING ONCE VALUE");
+ mapObj4.put("TRACK MAP STRING ONCE", "SHOULD NOT SEE ME");
+ mapObj5.put("TRACK MAP CUSTOM OBJECT", mixpanel);
+
+ try {
+ JSONObject message;
+ JSONObject properties;
+
+ mixpanel.track("event1", null);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event1", message.getString("event"));
+
+ mixpanel.track("event2", jsonObj1);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event2", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertEquals(
+ jsonObj1.getString("TRACK JSON STRING"), properties.getString("TRACK JSON STRING"));
+
+ mixpanel.trackMap("event3", null);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event3", message.getString("event"));
+
+ mixpanel.trackMap("event4", mapObj1);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event4", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertEquals(mapObj1.get("TRACK MAP STRING"), properties.getString("TRACK MAP STRING"));
+
+ mixpanel.registerSuperProperties(jsonObj2);
+ mixpanel.registerSuperPropertiesOnce(jsonObj3);
+ mixpanel.registerSuperPropertiesOnce(jsonObj4);
+ mixpanel.registerSuperPropertiesMap(mapObj2);
+ mixpanel.registerSuperPropertiesOnceMap(mapObj3);
+ mixpanel.registerSuperPropertiesOnceMap(mapObj4);
+
+ mixpanel.track("event5", null);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event5", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertEquals(jsonObj2.getInt("TRACK JSON INT"), properties.getInt("TRACK JSON INT"));
+ assertEquals(
+ jsonObj3.getString("TRACK JSON STRING ONCE"),
+ properties.getString("TRACK JSON STRING ONCE"));
+ assertEquals(mapObj2.get("TRACK MAP INT"), properties.getInt("TRACK MAP INT"));
+ assertEquals(
+ mapObj3.get("TRACK MAP STRING ONCE"), properties.getString("TRACK MAP STRING ONCE"));
+
+ mixpanel.unregisterSuperProperty("TRACK JSON INT");
+ mixpanel.track("event6", null);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event6", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertFalse(properties.has("TRACK JSON INT"));
+
+ mixpanel.clearSuperProperties();
+ mixpanel.track("event7", null);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event7", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertFalse(properties.has("TRACK JSON STRING ONCE"));
+
+ mixpanel.track("event8", jsonObj5);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event8", message.getString("event"));
+ properties = message.getJSONObject("properties");
+ assertEquals(jsonObj5.get("TRACK JSON NULL"), properties.get("TRACK JSON NULL"));
+
+ mixpanel.trackMap("event contains custom object", mapObj5);
+ message = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("event contains custom object", message.getString("event"));
+ } catch (InterruptedException e) {
+ fail("Unexpected interruption");
}
-
- @Test
- public void testPeopleMessageOperations() throws JSONException {
- final List messages = new ArrayList();
-
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- }
+ }
+
+ @Test
+ public void testPeopleMessageOperations() throws JSONException {
+ final List messages =
+ new ArrayList();
+
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "TEST TOKEN testIdentifyAfterSet") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "TEST TOKEN testIdentifyAfterSet") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
};
- Map mapObj1 = new HashMap<>();
- mapObj1.put("SET MAP INT", 1);
- Map mapObj2 = new HashMap<>();
- mapObj2.put("SET ONCE MAP STR", "SET ONCE MAP VALUE");
-
- mixpanel.identify("TEST IDENTITY");
-
- mixpanel.getPeople().set("SET NAME", "SET VALUE");
- mixpanel.getPeople().setMap(mapObj1);
- mixpanel.getPeople().increment("INCREMENT NAME", 1);
- mixpanel.getPeople().append("APPEND NAME", "APPEND VALUE");
- mixpanel.getPeople().setOnce("SET ONCE NAME", "SET ONCE VALUE");
- mixpanel.getPeople().setOnceMap(mapObj2);
- mixpanel.getPeople().union("UNION NAME", new JSONArray("[100]"));
- mixpanel.getPeople().unset("UNSET NAME");
- mixpanel.getPeople().trackCharge(100, new JSONObject("{\"name\": \"val\"}"));
- mixpanel.getPeople().clearCharges();
- mixpanel.getPeople().deleteUser();
-
- JSONObject setMessage = messages.get(0).getMessage().getJSONObject("$set");
- assertEquals("SET VALUE", setMessage.getString("SET NAME"));
-
- JSONObject setMapMessage = messages.get(1).getMessage().getJSONObject("$set");
- assertEquals(mapObj1.get("SET MAP INT"), setMapMessage.getInt("SET MAP INT"));
-
- JSONObject addMessage = messages.get(2).getMessage().getJSONObject("$add");
- assertEquals(1, addMessage.getInt("INCREMENT NAME"));
-
- JSONObject appendMessage = messages.get(3).getMessage().getJSONObject("$append");
- assertEquals("APPEND VALUE", appendMessage.get("APPEND NAME"));
-
- JSONObject setOnceMessage = messages.get(4).getMessage().getJSONObject("$set_once");
- assertEquals("SET ONCE VALUE", setOnceMessage.getString("SET ONCE NAME"));
-
- JSONObject setOnceMapMessage = messages.get(5).getMessage().getJSONObject("$set_once");
- assertEquals(mapObj2.get("SET ONCE MAP STR"), setOnceMapMessage.getString("SET ONCE MAP STR"));
-
- JSONObject unionMessage = messages.get(6).getMessage().getJSONObject("$union");
- JSONArray unionValues = unionMessage.getJSONArray("UNION NAME");
- assertEquals(1, unionValues.length());
- assertEquals(100, unionValues.getInt(0));
-
- JSONArray unsetMessage = messages.get(7).getMessage().getJSONArray("$unset");
- assertEquals(1, unsetMessage.length());
- assertEquals("UNSET NAME", unsetMessage.get(0));
-
- JSONObject trackChargeMessage = messages.get(8).getMessage().getJSONObject("$append");
- JSONObject transaction = trackChargeMessage.getJSONObject("$transactions");
- assertEquals(100.0d, transaction.getDouble("$amount"), 0);
-
- JSONArray clearChargesMessage = messages.get(9).getMessage().getJSONArray("$unset");
- assertEquals(1, clearChargesMessage.length());
- assertEquals("$transactions", clearChargesMessage.getString(0));
-
- assertTrue(messages.get(10).getMessage().has("$delete"));
- }
+ Map mapObj1 = new HashMap<>();
+ mapObj1.put("SET MAP INT", 1);
+ Map mapObj2 = new HashMap<>();
+ mapObj2.put("SET ONCE MAP STR", "SET ONCE MAP VALUE");
+
+ mixpanel.identify("TEST IDENTITY");
+
+ mixpanel.getPeople().set("SET NAME", "SET VALUE");
+ mixpanel.getPeople().setMap(mapObj1);
+ mixpanel.getPeople().increment("INCREMENT NAME", 1);
+ mixpanel.getPeople().append("APPEND NAME", "APPEND VALUE");
+ mixpanel.getPeople().setOnce("SET ONCE NAME", "SET ONCE VALUE");
+ mixpanel.getPeople().setOnceMap(mapObj2);
+ mixpanel.getPeople().union("UNION NAME", new JSONArray("[100]"));
+ mixpanel.getPeople().unset("UNSET NAME");
+ mixpanel.getPeople().trackCharge(100, new JSONObject("{\"name\": \"val\"}"));
+ mixpanel.getPeople().clearCharges();
+ mixpanel.getPeople().deleteUser();
+
+ JSONObject setMessage = messages.get(0).getMessage().getJSONObject("$set");
+ assertEquals("SET VALUE", setMessage.getString("SET NAME"));
+
+ JSONObject setMapMessage = messages.get(1).getMessage().getJSONObject("$set");
+ assertEquals(mapObj1.get("SET MAP INT"), setMapMessage.getInt("SET MAP INT"));
+
+ JSONObject addMessage = messages.get(2).getMessage().getJSONObject("$add");
+ assertEquals(1, addMessage.getInt("INCREMENT NAME"));
+
+ JSONObject appendMessage = messages.get(3).getMessage().getJSONObject("$append");
+ assertEquals("APPEND VALUE", appendMessage.get("APPEND NAME"));
+
+ JSONObject setOnceMessage = messages.get(4).getMessage().getJSONObject("$set_once");
+ assertEquals("SET ONCE VALUE", setOnceMessage.getString("SET ONCE NAME"));
+
+ JSONObject setOnceMapMessage = messages.get(5).getMessage().getJSONObject("$set_once");
+ assertEquals(mapObj2.get("SET ONCE MAP STR"), setOnceMapMessage.getString("SET ONCE MAP STR"));
+
+ JSONObject unionMessage = messages.get(6).getMessage().getJSONObject("$union");
+ JSONArray unionValues = unionMessage.getJSONArray("UNION NAME");
+ assertEquals(1, unionValues.length());
+ assertEquals(100, unionValues.getInt(0));
+
+ JSONArray unsetMessage = messages.get(7).getMessage().getJSONArray("$unset");
+ assertEquals(1, unsetMessage.length());
+ assertEquals("UNSET NAME", unsetMessage.get(0));
+
+ JSONObject trackChargeMessage = messages.get(8).getMessage().getJSONObject("$append");
+ JSONObject transaction = trackChargeMessage.getJSONObject("$transactions");
+ assertEquals(100.0d, transaction.getDouble("$amount"), 0);
+
+ JSONArray clearChargesMessage = messages.get(9).getMessage().getJSONArray("$unset");
+ assertEquals(1, clearChargesMessage.length());
+ assertEquals("$transactions", clearChargesMessage.getString(0));
+
+ assertTrue(messages.get(10).getMessage().has("$delete"));
+ }
+
+ @Test
+ public void testGroupOperations() throws JSONException {
+ final List messages =
+ new ArrayList();
+
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void groupMessage(GroupDescription heard) {
+ messages.add(heard);
+ }
+ };
- @Test
- public void testGroupOperations() throws JSONException {
- final List messages = new ArrayList();
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "TEST TOKEN testGroupOperations") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ };
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void groupMessage(GroupDescription heard) {
- messages.add(heard);
- }
+ Map mapObj1 = new HashMap<>();
+ mapObj1.put("SET MAP INT", 1);
+ Map mapObj2 = new HashMap<>();
+ mapObj2.put("SET ONCE MAP STR", "SET ONCE MAP VALUE");
+
+ String groupKey = "group key";
+ String groupID = "group id";
+
+ mixpanel.getGroup(groupKey, groupID).set("SET NAME", "SET VALUE");
+ mixpanel.getGroup(groupKey, groupID).setMap(mapObj1);
+ mixpanel.getGroup(groupKey, groupID).setOnce("SET ONCE NAME", "SET ONCE VALUE");
+ mixpanel.getGroup(groupKey, groupID).setOnceMap(mapObj2);
+ mixpanel.getGroup(groupKey, groupID).union("UNION NAME", new JSONArray("[100]"));
+ mixpanel.getGroup(groupKey, groupID).unset("UNSET NAME");
+ mixpanel.getGroup(groupKey, groupID).deleteGroup();
+
+ JSONObject setMessage = messages.get(0).getMessage();
+ assertEquals(setMessage.getString("$group_key"), groupKey);
+ assertEquals(setMessage.getString("$group_id"), groupID);
+ assertEquals("SET VALUE", setMessage.getJSONObject("$set").getString("SET NAME"));
+
+ JSONObject setMapMessage = messages.get(1).getMessage();
+ assertEquals(setMapMessage.getString("$group_key"), groupKey);
+ assertEquals(setMapMessage.getString("$group_id"), groupID);
+ assertEquals(
+ mapObj1.get("SET MAP INT"), setMapMessage.getJSONObject("$set").getInt("SET MAP INT"));
+
+ JSONObject setOnceMessage = messages.get(2).getMessage();
+ assertEquals(setOnceMessage.getString("$group_key"), groupKey);
+ assertEquals(setOnceMessage.getString("$group_id"), groupID);
+ assertEquals(
+ "SET ONCE VALUE", setOnceMessage.getJSONObject("$set_once").getString("SET ONCE NAME"));
+
+ JSONObject setOnceMapMessage = messages.get(3).getMessage();
+ assertEquals(setOnceMapMessage.getString("$group_key"), groupKey);
+ assertEquals(setOnceMapMessage.getString("$group_id"), groupID);
+ assertEquals(
+ mapObj2.get("SET ONCE MAP STR"),
+ setOnceMapMessage.getJSONObject("$set_once").getString("SET ONCE MAP STR"));
+
+ JSONObject unionMessage = messages.get(4).getMessage();
+ assertEquals(unionMessage.getString("$group_key"), groupKey);
+ assertEquals(unionMessage.getString("$group_id"), groupID);
+ JSONArray unionValues = unionMessage.getJSONObject("$union").getJSONArray("UNION NAME");
+ assertEquals(1, unionValues.length());
+ assertEquals(100, unionValues.getInt(0));
+
+ JSONObject unsetMessage = messages.get(5).getMessage();
+ assertEquals(unsetMessage.getString("$group_key"), groupKey);
+ assertEquals(unsetMessage.getString("$group_id"), groupID);
+ JSONArray unsetValues = unsetMessage.getJSONArray("$unset");
+ assertEquals(1, unsetValues.length());
+ assertEquals("UNSET NAME", unsetValues.get(0));
+
+ JSONObject deleteMessage = messages.get(6).getMessage();
+ assertEquals(deleteMessage.getString("$group_key"), groupKey);
+ assertEquals(deleteMessage.getString("$group_id"), groupID);
+ assertTrue(deleteMessage.has("$delete"));
+ }
+
+ @Test
+ public void testIdentifyAfterSet() throws InterruptedException, JSONException {
+ String token = "TEST TOKEN testIdentifyAfterSet";
+ final List messages =
+ new ArrayList();
+ final BlockingQueue anonymousUpdates = new LinkedBlockingQueue();
+ final BlockingQueue peopleUpdates = new LinkedBlockingQueue();
+
+ final MPDbAdapter mockAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public int addJSON(JSONObject j, String token, Table table) {
+ if (table == Table.ANONYMOUS_PEOPLE) {
+ anonymousUpdates.add(j);
+ } else if (table == Table.PEOPLE) {
+ peopleUpdates.add(j);
+ }
+ return super.addJSON(j, token, table);
+ }
+ };
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ super.peopleMessage(heard);
+ }
+
+ @Override
+ public void pushAnonymousPeopleMessage(
+ PushAnonymousPeopleDescription pushAnonymousPeopleDescription) {
+ messages.add(pushAnonymousPeopleDescription);
+ super.pushAnonymousPeopleMessage(pushAnonymousPeopleDescription);
+ }
+
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mockAdapter;
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "TEST TOKEN testGroupOperations") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, token) {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
};
- Map mapObj1 = new HashMap<>();
- mapObj1.put("SET MAP INT", 1);
- Map mapObj2 = new HashMap<>();
- mapObj2.put("SET ONCE MAP STR", "SET ONCE MAP VALUE");
-
- String groupKey = "group key";
- String groupID = "group id";
-
- mixpanel.getGroup(groupKey, groupID).set("SET NAME", "SET VALUE");
- mixpanel.getGroup(groupKey, groupID).setMap(mapObj1);
- mixpanel.getGroup(groupKey, groupID).setOnce("SET ONCE NAME", "SET ONCE VALUE");
- mixpanel.getGroup(groupKey, groupID).setOnceMap(mapObj2);
- mixpanel.getGroup(groupKey, groupID).union("UNION NAME", new JSONArray("[100]"));
- mixpanel.getGroup(groupKey, groupID).unset("UNSET NAME");
- mixpanel.getGroup(groupKey, groupID).deleteGroup();
-
- JSONObject setMessage = messages.get(0).getMessage();
- assertEquals(setMessage.getString("$group_key"), groupKey);
- assertEquals(setMessage.getString("$group_id"), groupID);
- assertEquals("SET VALUE",
- setMessage.getJSONObject("$set").getString("SET NAME"));
-
- JSONObject setMapMessage = messages.get(1).getMessage();
- assertEquals(setMapMessage.getString("$group_key"), groupKey);
- assertEquals(setMapMessage.getString("$group_id"), groupID);
- assertEquals(mapObj1.get("SET MAP INT"),
- setMapMessage.getJSONObject("$set").getInt("SET MAP INT"));
-
- JSONObject setOnceMessage = messages.get(2).getMessage();
- assertEquals(setOnceMessage.getString("$group_key"), groupKey);
- assertEquals(setOnceMessage.getString("$group_id"), groupID);
- assertEquals("SET ONCE VALUE",
- setOnceMessage.getJSONObject("$set_once").getString("SET ONCE NAME"));
-
- JSONObject setOnceMapMessage = messages.get(3).getMessage();
- assertEquals(setOnceMapMessage.getString("$group_key"), groupKey);
- assertEquals(setOnceMapMessage.getString("$group_id"), groupID);
- assertEquals(mapObj2.get("SET ONCE MAP STR"),
- setOnceMapMessage.getJSONObject("$set_once").getString("SET ONCE MAP STR"));
-
- JSONObject unionMessage = messages.get(4).getMessage();
- assertEquals(unionMessage.getString("$group_key"), groupKey);
- assertEquals(unionMessage.getString("$group_id"), groupID);
- JSONArray unionValues = unionMessage.getJSONObject("$union").getJSONArray("UNION NAME");
- assertEquals(1, unionValues.length());
- assertEquals(100, unionValues.getInt(0));
-
- JSONObject unsetMessage = messages.get(5).getMessage();
- assertEquals(unsetMessage.getString("$group_key"), groupKey);
- assertEquals(unsetMessage.getString("$group_id"), groupID);
- JSONArray unsetValues = unsetMessage.getJSONArray("$unset");
- assertEquals(1, unsetValues.length());
- assertEquals("UNSET NAME", unsetValues.get(0));
-
- JSONObject deleteMessage = messages.get(6).getMessage();
- assertEquals(deleteMessage.getString("$group_key"), groupKey);
- assertEquals(deleteMessage.getString("$group_id"), groupID);
- assertTrue(deleteMessage.has("$delete"));
+ MixpanelAPI.People people = mixpanel.getPeople();
+ people.increment("the prop", 0L);
+ people.append("the prop", 1);
+ people.set("the prop", 2);
+ people.increment("the prop", 3L);
+ people.append("the prop", 5);
+
+ assertEquals(
+ 0L,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$add")
+ .getLong("the prop"));
+ assertEquals(
+ 1,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$append")
+ .get("the prop"));
+ assertEquals(
+ 2,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$set")
+ .get("the prop"));
+ assertEquals(
+ 3L,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$add")
+ .getLong("the prop"));
+ assertEquals(
+ 5,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$append")
+ .get("the prop"));
+ assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+ assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+
+ String deviceId = mixpanel.getAnonymousId();
+ mixpanel.identify("Personal Identity");
+ people.set("the prop identified", "prop value identified");
+
+ assertEquals(
+ "prop value identified",
+ peopleUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$set")
+ .getString("the prop identified"));
+ assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+ assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+
+ String[] storedAnonymous =
+ mockAdapter.generateDataString(MPDbAdapter.Table.ANONYMOUS_PEOPLE, token);
+ assertNull(storedAnonymous);
+
+ String[] storedPeople = mockAdapter.generateDataString(MPDbAdapter.Table.PEOPLE, token);
+ assertEquals(6, Integer.valueOf(storedPeople[2]).intValue());
+ JSONArray data = new JSONArray(storedPeople[1]);
+ for (int i = 0; i < data.length(); i++) {
+ JSONObject j = data.getJSONObject(i);
+ assertEquals("Personal Identity", j.getString("$distinct_id"));
+ assertEquals(deviceId, j.getString("$device_id"));
}
-
- @Test
- public void testIdentifyAfterSet() throws InterruptedException, JSONException {
- String token = "TEST TOKEN testIdentifyAfterSet";
- final List messages = new ArrayList();
- final BlockingQueue anonymousUpdates = new LinkedBlockingQueue();
- final BlockingQueue peopleUpdates = new LinkedBlockingQueue();
-
- final MPDbAdapter mockAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public int addJSON(JSONObject j, String token, Table table) {
- if (table == Table.ANONYMOUS_PEOPLE) {
- anonymousUpdates.add(j);
- } else if (table == Table.PEOPLE) {
- peopleUpdates.add(j);
- }
- return super.addJSON(j, token, table);
- }
+ }
+
+ @Test
+ public void testIdentifyAfterSetToAnonymousId() throws InterruptedException, JSONException {
+ String token = "TEST TOKEN testIdentifyAfterSet";
+ final List messages =
+ new ArrayList();
+ final BlockingQueue anonymousUpdates = new LinkedBlockingQueue();
+ final BlockingQueue peopleUpdates = new LinkedBlockingQueue();
+
+ final MPDbAdapter mockAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public int addJSON(JSONObject j, String token, Table table) {
+ if (table == Table.ANONYMOUS_PEOPLE) {
+ anonymousUpdates.add(j);
+ } else if (table == Table.PEOPLE) {
+ peopleUpdates.add(j);
+ }
+ return super.addJSON(j, token, table);
+ }
};
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- super.peopleMessage(heard);
- }
-
- @Override
- public void pushAnonymousPeopleMessage(PushAnonymousPeopleDescription pushAnonymousPeopleDescription) {
- messages.add(pushAnonymousPeopleDescription);
- super.pushAnonymousPeopleMessage(pushAnonymousPeopleDescription);
- }
-
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mockAdapter;
- }
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ super.peopleMessage(heard);
+ }
+
+ @Override
+ public void pushAnonymousPeopleMessage(
+ PushAnonymousPeopleDescription pushAnonymousPeopleDescription) {
+ messages.add(pushAnonymousPeopleDescription);
+ super.pushAnonymousPeopleMessage(pushAnonymousPeopleDescription);
+ }
+
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mockAdapter;
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, token) {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ MixpanelAPI mixpanel =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, token) {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
};
- MixpanelAPI.People people = mixpanel.getPeople();
- people.increment("the prop", 0L);
- people.append("the prop", 1);
- people.set("the prop", 2);
- people.increment("the prop", 3L);
- people.append("the prop", 5);
-
- assertEquals(0L, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$add").getLong("the prop"));
- assertEquals(1, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$append").get("the prop"));
- assertEquals(2, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$set").get("the prop"));
- assertEquals(3L, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$add").getLong("the prop"));
- assertEquals(5, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$append").get("the prop"));
- assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
- assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
-
- String deviceId = mixpanel.getAnonymousId();
- mixpanel.identify("Personal Identity");
- people.set("the prop identified", "prop value identified");
-
- assertEquals("prop value identified", peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$set").getString("the prop identified"));
- assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
- assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
-
- String[] storedAnonymous = mockAdapter.generateDataString(MPDbAdapter.Table.ANONYMOUS_PEOPLE, token);
- assertNull(storedAnonymous);
-
- String[] storedPeople = mockAdapter.generateDataString(MPDbAdapter.Table.PEOPLE, token);
- assertEquals(6, Integer.valueOf(storedPeople[2]).intValue());
- JSONArray data = new JSONArray(storedPeople[1]);
- for (int i=0; i < data.length(); i++) {
- JSONObject j = data.getJSONObject(i);
- assertEquals("Personal Identity", j.getString("$distinct_id"));
- assertEquals(deviceId, j.getString("$device_id"));
- }
+ MixpanelAPI.People people = mixpanel.getPeople();
+ people.increment("the prop", 0L);
+ people.append("the prop", 1);
+ people.set("the prop", 2);
+ people.increment("the prop", 3L);
+ people.append("the prop", 5);
+
+ assertEquals(
+ 0L,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$add")
+ .getLong("the prop"));
+ assertEquals(
+ 1,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$append")
+ .get("the prop"));
+ assertEquals(
+ 2,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$set")
+ .get("the prop"));
+ assertEquals(
+ 3L,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$add")
+ .getLong("the prop"));
+ assertEquals(
+ 5,
+ anonymousUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$append")
+ .get("the prop"));
+ assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+ assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+
+ String deviceId = mixpanel.getAnonymousId();
+ mixpanel.identify(mixpanel.getDistinctId());
+ people.set("the prop identified", "prop value identified");
+ assertNull(mixpanel.getUserId());
+
+ assertEquals(
+ "prop value identified",
+ peopleUpdates
+ .poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS)
+ .getJSONObject("$set")
+ .getString("the prop identified"));
+ assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+ assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
+
+ String[] storedAnonymous =
+ mockAdapter.generateDataString(MPDbAdapter.Table.ANONYMOUS_PEOPLE, token);
+ assertNull(storedAnonymous);
+
+ String[] storedPeople = mockAdapter.generateDataString(MPDbAdapter.Table.PEOPLE, token);
+ assertEquals(6, Integer.valueOf(storedPeople[2]).intValue());
+ JSONArray data = new JSONArray(storedPeople[1]);
+ for (int i = 0; i < data.length(); i++) {
+ JSONObject j = data.getJSONObject(i);
+ assertEquals("$device:" + deviceId, j.getString("$distinct_id"));
+ assertEquals(deviceId, j.getString("$device_id"));
}
-
- @Test
- public void testIdentifyAfterSetToAnonymousId() throws InterruptedException, JSONException {
- String token = "TEST TOKEN testIdentifyAfterSet";
- final List messages = new ArrayList();
- final BlockingQueue anonymousUpdates = new LinkedBlockingQueue();
- final BlockingQueue peopleUpdates = new LinkedBlockingQueue();
-
- final MPDbAdapter mockAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public int addJSON(JSONObject j, String token, Table table) {
- if (table == Table.ANONYMOUS_PEOPLE) {
- anonymousUpdates.add(j);
- } else if (table == Table.PEOPLE) {
- peopleUpdates.add(j);
- }
- return super.addJSON(j, token, table);
- }
+ }
+
+ @Test
+ public void testIdentifyAndGetDistinctId() {
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Identify Test Token");
+
+ String generatedId = metrics.getDistinctId();
+ assertThat(generatedId, startsWith("$device:"));
+ assertEquals(generatedId, "$device:" + metrics.getAnonymousId());
+
+ assertNull(metrics.getUserId());
+ assertNull(metrics.getPeople().getDistinctId());
+
+ metrics.identify("Events Id");
+ assertEquals("Events Id", metrics.getDistinctId());
+ assertEquals("Events Id", metrics.getUserId());
+ assertEquals("Events Id", metrics.getPeople().getDistinctId());
+ }
+
+ @Test
+ public void testIdentifyToCurrentAnonymousDistinctId() {
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Identify Test Token");
+
+ String generatedId = metrics.getDistinctId();
+ assertThat(generatedId, startsWith("$device:"));
+ assertEquals(generatedId, "$device:" + metrics.getAnonymousId());
+
+ assertNull(metrics.getUserId());
+ assertNull(metrics.getPeople().getDistinctId());
+
+ metrics.identify(metrics.getDistinctId());
+ assertEquals(generatedId, metrics.getDistinctId());
+ assertNull(metrics.getUserId());
+ assertEquals(generatedId, metrics.getPeople().getDistinctId());
+ }
+
+ @Test
+ public void testIdentifyAndCheckUserIDAndDeviceID() {
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Identify Test Token");
+
+ String generatedId = metrics.getAnonymousId();
+ assertNotNull(metrics.getAnonymousId());
+ String eventsDistinctId = metrics.getDistinctId();
+ assertEquals("$device:" + generatedId, eventsDistinctId);
+ assertNull(metrics.getUserId());
+ assertNull(metrics.getPeople().getDistinctId());
+
+ metrics.identify("Distinct Id");
+ assertEquals("Distinct Id", metrics.getDistinctId());
+ assertEquals(generatedId, metrics.getAnonymousId());
+ assertEquals("Distinct Id", metrics.getPeople().getDistinctId());
+
+ // once its reset we will only have generated id but user id should be null
+ metrics.reset();
+ String generatedId2 = metrics.getAnonymousId();
+ assertNotNull(generatedId2);
+ assertNotSame(generatedId, generatedId2);
+ assertEquals("$device:" + generatedId2, metrics.getDistinctId());
+ assertNull(metrics.getUserId());
+ }
+
+ @Test
+ public void testMessageQueuing() {
+ final BlockingQueue messages = new LinkedBlockingQueue();
+ final SynchronizedReference isIdentifiedRef = new SynchronizedReference();
+ isIdentifiedRef.set(false);
+
+ final MPDbAdapter mockAdapter =
+ new MPDbAdapter(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
+ try {
+ messages.put("TABLE " + table.getName());
+ messages.put(message.toString());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ return super.addJSON(message, token, table);
+ }
};
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- super.peopleMessage(heard);
- }
-
- @Override
- public void pushAnonymousPeopleMessage(PushAnonymousPeopleDescription pushAnonymousPeopleDescription) {
- messages.add(pushAnonymousPeopleDescription);
- super.pushAnonymousPeopleMessage(pushAnonymousPeopleDescription);
- }
-
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mockAdapter;
- }
+ mockAdapter.cleanupEvents(Long.MAX_VALUE, MPDbAdapter.Table.EVENTS);
+ mockAdapter.cleanupEvents(Long.MAX_VALUE, MPDbAdapter.Table.PEOPLE);
+
+ final RemoteService mockPoster =
+ new HttpService() {
+ @Override
+ public byte[] performRequest(
+ @NonNull String endpointUrl,
+ @Nullable ProxyServerInteractor interactor,
+ @Nullable Map params, // Used only if requestBodyBytes is null
+ @Nullable Map headers,
+ @Nullable byte[] requestBodyBytes, // If provided, send this as raw body
+ @Nullable SSLSocketFactory socketFactory) {
+ final boolean isIdentified = isIdentifiedRef.get();
+ assertTrue(params.containsKey("data"));
+ final String decoded = Base64Coder.decodeString(params.get("data").toString());
+
+ try {
+ messages.put("SENT FLUSH " + endpointUrl);
+ messages.put(decoded);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ return TestUtils.bytes("1\n");
+ }
};
- MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, token) {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ final MPConfig mockConfig =
+ new MPConfig(
+ new Bundle(), InstrumentationRegistry.getInstrumentation().getContext(), null) {
+ @Override
+ public int getFlushInterval() {
+ return -1;
+ }
+
+ @Override
+ public int getBulkUploadLimit() {
+ return 40;
+ }
+
+ @Override
+ public String getEventsEndpoint() {
+ return "EVENTS_ENDPOINT";
+ }
+
+ @Override
+ public String getPeopleEndpoint() {
+ return "PEOPLE_ENDPOINT";
+ }
+
+ @Override
+ public String getGroupsEndpoint() {
+ return "GROUPS_ENDPOINT";
+ }
+
+ @Override
+ public boolean getDisableAppOpenEvent() {
+ return true;
+ }
};
- MixpanelAPI.People people = mixpanel.getPeople();
- people.increment("the prop", 0L);
- people.append("the prop", 1);
- people.set("the prop", 2);
- people.increment("the prop", 3L);
- people.append("the prop", 5);
-
- assertEquals(0L, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$add").getLong("the prop"));
- assertEquals(1, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$append").get("the prop"));
- assertEquals(2, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$set").get("the prop"));
- assertEquals(3L, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$add").getLong("the prop"));
- assertEquals(5, anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$append").get("the prop"));
- assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
- assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
-
- String deviceId = mixpanel.getAnonymousId();
- mixpanel.identify(mixpanel.getDistinctId());
- people.set("the prop identified", "prop value identified");
- assertNull(mixpanel.getUserId());
-
- assertEquals("prop value identified", peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS).getJSONObject("$set").getString("the prop identified"));
- assertNull(peopleUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
- assertNull(anonymousUpdates.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS));
-
- String[] storedAnonymous = mockAdapter.generateDataString(MPDbAdapter.Table.ANONYMOUS_PEOPLE, token);
- assertNull(storedAnonymous);
-
- String[] storedPeople = mockAdapter.generateDataString(MPDbAdapter.Table.PEOPLE, token);
- assertEquals(6, Integer.valueOf(storedPeople[2]).intValue());
- JSONArray data = new JSONArray(storedPeople[1]);
- for (int i=0; i < data.length(); i++) {
- JSONObject j = data.getJSONObject(i);
- assertEquals("$device:" + deviceId, j.getString("$distinct_id"));
- assertEquals(deviceId, j.getString("$device_id"));
- }
- }
-
- @Test
- public void testIdentifyAndGetDistinctId() {
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Identify Test Token");
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(), mockConfig) {
+ @Override
+ protected MPDbAdapter makeDbAdapter(Context context) {
+ return mockAdapter;
+ }
+
+ @Override
+ protected RemoteService getPoster() {
+ return mockPoster;
+ }
+ };
- String generatedId = metrics.getDistinctId();
- assertThat(generatedId, startsWith("$device:"));
- assertEquals(generatedId, "$device:" + metrics.getAnonymousId());
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Test Message Queuing") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ };
- assertNull(metrics.getUserId());
- assertNull(metrics.getPeople().getDistinctId());
+ metrics.identify("EVENTS ID");
- metrics.identify("Events Id");
- assertEquals("Events Id", metrics.getDistinctId());
- assertEquals("Events Id", metrics.getUserId());
- assertEquals("Events Id", metrics.getPeople().getDistinctId());
+ // Test filling up the message queue
+ for (int i = 0; i < mockConfig.getBulkUploadLimit() - 2; i++) {
+ metrics.track("frequent event", null);
}
- @Test
- public void testIdentifyToCurrentAnonymousDistinctId() {
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Identify Test Token");
+ metrics.track("final event", null);
+ String expectedJSONMessage = "";
- String generatedId = metrics.getDistinctId();
- assertThat(generatedId, startsWith("$device:"));
- assertEquals(generatedId, "$device:" + metrics.getAnonymousId());
+ try {
+ String messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
- assertNull(metrics.getUserId());
- assertNull(metrics.getPeople().getDistinctId());
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONObject message = new JSONObject(expectedJSONMessage);
+ assertEquals("$identify", message.getString("event"));
- metrics.identify(metrics.getDistinctId());
- assertEquals(generatedId, metrics.getDistinctId());
- assertNull(metrics.getUserId());
- assertEquals(generatedId, metrics.getPeople().getDistinctId());
- }
+ for (int i = 0; i < mockConfig.getBulkUploadLimit() - 2; i++) {
+ messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
- @Test
- public void testIdentifyAndCheckUserIDAndDeviceID() {
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Identify Test Token");
-
- String generatedId = metrics.getAnonymousId();
- assertNotNull(metrics.getAnonymousId());
- String eventsDistinctId = metrics.getDistinctId();
- assertEquals("$device:" + generatedId, eventsDistinctId);
- assertNull(metrics.getUserId());
- assertNull(metrics.getPeople().getDistinctId());
-
- metrics.identify("Distinct Id");
- assertEquals("Distinct Id", metrics.getDistinctId());
- assertEquals(generatedId, metrics.getAnonymousId());
- assertEquals("Distinct Id", metrics.getPeople().getDistinctId());
-
- // once its reset we will only have generated id but user id should be null
- metrics.reset();
- String generatedId2 = metrics.getAnonymousId();
- assertNotNull(generatedId2);
- assertNotSame(generatedId, generatedId2);
- assertEquals("$device:" + generatedId2, metrics.getDistinctId());
- assertNull(metrics.getUserId());
- }
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ message = new JSONObject(expectedJSONMessage);
+ assertEquals("frequent event", message.getString("event"));
+ }
- @Test
- public void testMessageQueuing() {
- final BlockingQueue messages = new LinkedBlockingQueue();
- final SynchronizedReference isIdentifiedRef = new SynchronizedReference();
- isIdentifiedRef.set(false);
-
- final MPDbAdapter mockAdapter = new MPDbAdapter(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) {
- try {
- messages.put("TABLE " + table.getName());
- messages.put(message.toString());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
- return super.addJSON(message, token, table);
- }
- };
- mockAdapter.cleanupEvents(Long.MAX_VALUE, MPDbAdapter.Table.EVENTS);
- mockAdapter.cleanupEvents(Long.MAX_VALUE, MPDbAdapter.Table.PEOPLE);
-
- final RemoteService mockPoster = new HttpService() {
- @Override
- public byte[] performRequest(
- @NonNull String endpointUrl,
- @Nullable ProxyServerInteractor interactor,
- @Nullable Map params, // Used only if requestBodyBytes is null
- @Nullable Map headers,
- @Nullable byte[] requestBodyBytes, // If provided, send this as raw body
- @Nullable SSLSocketFactory socketFactory)
- {
- final boolean isIdentified = isIdentifiedRef.get();
- assertTrue(params.containsKey("data"));
- final String decoded = Base64Coder.decodeString(params.get("data").toString());
-
- try {
- messages.put("SENT FLUSH " + endpointUrl);
- messages.put(decoded);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- return TestUtils.bytes("1\n");
- }
- };
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ message = new JSONObject(expectedJSONMessage);
+ assertEquals("final event", message.getString("event"));
+ String messageFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("SENT FLUSH EVENTS_ENDPOINT", messageFlush);
- final MPConfig mockConfig = new MPConfig(new Bundle(), InstrumentationRegistry.getInstrumentation().getContext(), null) {
- @Override
- public int getFlushInterval() {
- return -1;
- }
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONArray bigFlush = new JSONArray(expectedJSONMessage);
+ assertEquals(mockConfig.getBulkUploadLimit(), bigFlush.length());
- @Override
- public int getBulkUploadLimit() {
- return 40;
- }
+ metrics.track("next wave", null);
+ metrics.flush();
- @Override
- public String getEventsEndpoint() {
- return "EVENTS_ENDPOINT";
- }
+ String nextWaveTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), nextWaveTable);
- @Override
- public String getPeopleEndpoint() {
- return "PEOPLE_ENDPOINT";
- }
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONObject nextWaveMessage = new JSONObject(expectedJSONMessage);
+ assertEquals("next wave", nextWaveMessage.getString("event"));
- @Override
- public String getGroupsEndpoint() {
- return "GROUPS_ENDPOINT";
- }
+ String manualFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("SENT FLUSH EVENTS_ENDPOINT", manualFlush);
- @Override
- public boolean getDisableAppOpenEvent() { return true; }
- };
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONArray nextWave = new JSONArray(expectedJSONMessage);
+ assertEquals(1, nextWave.length());
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), mockConfig) {
- @Override
- protected MPDbAdapter makeDbAdapter(Context context) {
- return mockAdapter;
- }
+ JSONObject nextWaveEvent = nextWave.getJSONObject(0);
+ assertEquals("next wave", nextWaveEvent.getString("event"));
+ isIdentifiedRef.set(true);
+ metrics.identify("PEOPLE ID");
+ metrics.getPeople().set("prop", "yup");
+ metrics.flush();
- @Override
- protected RemoteService getPoster() {
- return mockPoster;
- }
- };
+ String peopleTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), peopleTable);
+ messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONObject peopleMessage = new JSONObject(expectedJSONMessage);
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Test Message Queuing") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
- };
+ assertEquals("PEOPLE ID", peopleMessage.getString("$distinct_id"));
+ assertEquals("yup", peopleMessage.getJSONObject("$set").getString("prop"));
- metrics.identify("EVENTS ID");
+ messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ String peopleFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("SENT FLUSH PEOPLE_ENDPOINT", peopleFlush);
- // Test filling up the message queue
- for (int i=0; i < mockConfig.getBulkUploadLimit() - 2; i++) {
- metrics.track("frequent event", null);
- }
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONArray peopleSent = new JSONArray(expectedJSONMessage);
+ assertEquals(1, peopleSent.length());
- metrics.track("final event", null);
- String expectedJSONMessage = "";
+ metrics.getGroup("testKey", "testID").set("prop", "yup");
+ metrics.flush();
- try {
- String messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
+ String groupsTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("TABLE " + MPDbAdapter.Table.GROUPS.getName(), groupsTable);
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONObject message = new JSONObject(expectedJSONMessage);
- assertEquals("$identify", message.getString("event"));
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONObject groupsMessage = new JSONObject(expectedJSONMessage);
- for (int i=0; i < mockConfig.getBulkUploadLimit() - 2; i++) {
- messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
+ assertEquals("testKey", groupsMessage.getString("$group_key"));
+ assertEquals("testID", groupsMessage.getString("$group_id"));
+ assertEquals("yup", groupsMessage.getJSONObject("$set").getString("prop"));
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- message = new JSONObject(expectedJSONMessage);
- assertEquals("frequent event", message.getString("event"));
- }
+ String groupsFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ assertEquals("SENT FLUSH GROUPS_ENDPOINT", groupsFlush);
- messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable);
+ expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
+ JSONArray groupsSent = new JSONArray(expectedJSONMessage);
+ assertEquals(1, groupsSent.length());
+ } catch (InterruptedException e) {
+ fail("Expected a log message about mixpanel communication but did not receive it.");
+ } catch (JSONException e) {
+ fail(
+ "Expected a JSON object message and got something silly instead: " + expectedJSONMessage);
+ }
+ }
+
+ @Test
+ public void testTrackCharge() {
+ final List messages = new ArrayList<>();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic()) {
+ throw new RuntimeException("Should not be called during this test");
+ }
+ }
+
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ }
+ };
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- message = new JSONObject(expectedJSONMessage);
- assertEquals("final event", message.getString("event"));
+ class ListeningAPI extends TestUtils.CleanMixpanelAPI {
+ public ListeningAPI(Context c, Future referrerPrefs, String token) {
+ super(c, referrerPrefs, token);
+ }
- String messageFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("SENT FLUSH EVENTS_ENDPOINT", messageFlush);
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ }
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONArray bigFlush = new JSONArray(expectedJSONMessage);
- assertEquals(mockConfig.getBulkUploadLimit(), bigFlush.length());
+ MixpanelAPI api =
+ new ListeningAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "TRACKCHARGE TEST TOKEN");
+ api.getPeople().identify("TRACKCHARGE PERSON");
+
+ JSONObject props;
+ try {
+ props = new JSONObject("{'$time':'Should override', 'Orange':'Banana'}");
+ } catch (JSONException e) {
+ throw new RuntimeException("Can't construct fixture for trackCharge test");
+ }
- metrics.track("next wave", null);
- metrics.flush();
+ api.getPeople().trackCharge(2.13, props);
+ assertEquals(messages.size(), 1);
- String nextWaveTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), nextWaveTable);
+ JSONObject message = messages.get(0).getMessage();
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONObject nextWaveMessage = new JSONObject(expectedJSONMessage);
- assertEquals("next wave", nextWaveMessage.getString("event"));
+ try {
+ JSONObject append = message.getJSONObject("$append");
+ JSONObject newTransaction = append.getJSONObject("$transactions");
+ assertEquals(newTransaction.optString("Orange"), "Banana");
+ assertEquals(newTransaction.optString("$time"), "Should override");
+ assertEquals(newTransaction.optDouble("$amount"), 2.13, 0);
+ } catch (JSONException e) {
+ fail("Transaction message had unexpected layout:\n" + message.toString());
+ }
+ }
+
+ @Test
+ public void testTrackWithSavedDistinctId() {
+ final String savedDistinctID = "saved_distinct_id";
+ final List messages = new ArrayList();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic() && !heard.getEventName().equals("$identify")) {
+ messages.add(heard);
+ }
+ }
+
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ }
+ };
- String manualFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("SENT FLUSH EVENTS_ENDPOINT", manualFlush);
+ class TestMixpanelAPI extends MixpanelAPI {
+ public TestMixpanelAPI(Context c, Future prefs, String token) {
+ super(c, prefs, token, false, null, true);
+ }
+
+ @Override
+ /* package */ PersistentIdentity getPersistentIdentity(
+ final Context context,
+ final Future referrerPreferences,
+ final String token,
+ final String instanceName) {
+ String instanceKey = instanceName != null ? instanceName : token;
+ final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
+ final SharedPreferences mpSharedPrefs =
+ context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
+ mpSharedPrefs
+ .edit()
+ .clear()
+ .putBoolean(token, true)
+ .putBoolean("has_launched", true)
+ .commit();
+ final String prefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI_" + instanceKey;
+ final SharedPreferences loadstorePrefs =
+ context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
+ loadstorePrefs
+ .edit()
+ .clear()
+ .putString("events_distinct_id", savedDistinctID)
+ .putString("people_distinct_id", savedDistinctID)
+ .commit();
+ return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
+ }
+
+ @Override
+ /* package */ boolean sendAppOpen() {
+ return false;
+ }
+
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ }
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONArray nextWave = new JSONArray(expectedJSONMessage);
- assertEquals(1, nextWave.length());
+ TestMixpanelAPI mpMetrics =
+ new TestMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "SAME TOKEN");
+ assertEquals(mpMetrics.getDistinctId(), savedDistinctID);
+ mpMetrics.identify("new_user");
+
+ mpMetrics.track("eventname", null);
+ mpMetrics.getPeople().set("people prop name", "Indeed");
+
+ assertEquals(2, messages.size());
+
+ AnalyticsMessages.EventDescription eventMessage =
+ (AnalyticsMessages.EventDescription) messages.get(0);
+ JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(1)).getMessage();
+
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ String deviceId = eventProps.getString("$device_id");
+ assertEquals(savedDistinctID, deviceId);
+ boolean hadPersistedDistinctId = eventProps.getBoolean("$had_persisted_distinct_id");
+ assertEquals(true, hadPersistedDistinctId);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- JSONObject nextWaveEvent = nextWave.getJSONObject(0);
- assertEquals("next wave", nextWaveEvent.getString("event"));
+ try {
+ String deviceId = peopleMessage.getString("$device_id");
+ boolean hadPersistedDistinctId = peopleMessage.getBoolean("$had_persisted_distinct_id");
+ assertEquals(savedDistinctID, deviceId);
+ assertEquals(true, hadPersistedDistinctId);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
+ messages.clear();
+ }
+
+ @Test
+ public void testSetAddRemoveGroup() {
+ final List messages = new ArrayList();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic()
+ && !heard.getEventName().equals("$identify")
+ && !heard.getEventName().equals("Integration")) {
+ messages.add(heard);
+ }
+ }
+
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ }
+ };
- isIdentifiedRef.set(true);
- metrics.identify("PEOPLE ID");
- metrics.getPeople().set("prop", "yup");
- metrics.flush();
+ class TestMixpanelAPI extends MixpanelAPI {
+ public TestMixpanelAPI(Context c, Future prefs, String token) {
+ super(c, prefs, token, false, null, true);
+ }
- String peopleTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), peopleTable);
- messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONObject peopleMessage = new JSONObject(expectedJSONMessage);
+ @Override
+ /* package */ boolean sendAppOpen() {
+ return false;
+ }
- assertEquals("PEOPLE ID", peopleMessage.getString("$distinct_id"));
- assertEquals("yup", peopleMessage.getJSONObject("$set").getString("prop"));
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ }
- messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- String peopleFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("SENT FLUSH PEOPLE_ENDPOINT", peopleFlush);
+ TestMixpanelAPI mpMetrics =
+ new TestMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "SAME TOKEN");
+ mpMetrics.identify("new_user");
+
+ int groupID = 42;
+ mpMetrics.setGroup("group_key", groupID);
+ mpMetrics.track("eventname", null);
+
+ assertEquals(2, messages.size());
+
+ JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(0)).getMessage();
+ AnalyticsMessages.EventDescription eventMessage =
+ (AnalyticsMessages.EventDescription) messages.get(1);
+
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ JSONArray groupIDs = eventProps.getJSONArray("group_key");
+ assertEquals((new JSONArray()).put(groupID), groupIDs);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONArray peopleSent = new JSONArray(expectedJSONMessage);
- assertEquals(1, peopleSent.length());
+ try {
+ JSONObject setMessage = peopleMessage.getJSONObject("$set");
+ assertEquals((new JSONArray()).put(groupID), setMessage.getJSONArray("group_key"));
+ } catch (JSONException e) {
+ fail("People message has an unexpected shape " + e);
+ }
- metrics.getGroup("testKey", "testID").set("prop", "yup");
- metrics.flush();
+ messages.clear();
+
+ int groupID2 = 77;
+ mpMetrics.addGroup("group_key", groupID2);
+ mpMetrics.track("eventname", null);
+ JSONArray expectedGroupIDs = new JSONArray();
+ expectedGroupIDs.put(groupID);
+ expectedGroupIDs.put(groupID2);
+
+ assertEquals(2, messages.size());
+ peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(0)).getMessage();
+ eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
+
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ JSONArray groupIDs = eventProps.getJSONArray("group_key");
+ assertEquals(expectedGroupIDs, groupIDs);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- String groupsTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("TABLE " + MPDbAdapter.Table.GROUPS.getName(), groupsTable);
+ try {
+ JSONObject unionMessage = peopleMessage.getJSONObject("$union");
+ assertEquals((new JSONArray()).put(groupID2), unionMessage.getJSONArray("group_key"));
+ } catch (JSONException e) {
+ fail("People message has an unexpected shape " + e);
+ }
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONObject groupsMessage = new JSONObject(expectedJSONMessage);
+ messages.clear();
+ mpMetrics.removeGroup("group_key", groupID2);
+ mpMetrics.track("eventname", null);
- assertEquals("testKey", groupsMessage.getString("$group_key"));
- assertEquals("testID", groupsMessage.getString("$group_id"));
- assertEquals("yup", groupsMessage.getJSONObject("$set").getString("prop"));
+ assertEquals(2, messages.size());
+ peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(0)).getMessage();
+ eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
- String groupsFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- assertEquals("SENT FLUSH GROUPS_ENDPOINT", groupsFlush);
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ JSONArray groupIDs = eventProps.getJSONArray("group_key");
+ assertEquals((new JSONArray()).put(groupID), groupIDs);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS);
- JSONArray groupsSent = new JSONArray(expectedJSONMessage);
- assertEquals(1, groupsSent.length());
- } catch (InterruptedException e) {
- fail("Expected a log message about mixpanel communication but did not receive it.");
- } catch (JSONException e) {
- fail("Expected a JSON object message and got something silly instead: " + expectedJSONMessage);
- }
+ try {
+ JSONObject removeMessage = peopleMessage.getJSONObject("$remove");
+ assertEquals(groupID2, removeMessage.getInt("group_key"));
+ } catch (JSONException e) {
+ fail("People message has an unexpected shape " + e);
}
- @Test
- public void testTrackCharge() {
- final List messages = new ArrayList<>();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic()) {
- throw new RuntimeException("Should not be called during this test");
- }
- }
+ messages.clear();
+ mpMetrics.removeGroup("group_key", groupID);
+ mpMetrics.track("eventname", null);
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- }
- };
+ assertEquals(2, messages.size());
+ peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(0)).getMessage();
+ eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
- class ListeningAPI extends TestUtils.CleanMixpanelAPI {
- public ListeningAPI(Context c, Future referrerPrefs, String token) {
- super(c, referrerPrefs, token);
- }
+ JSONObject eventProps = eventMessage.getProperties();
+ assertFalse(eventProps.has("group_key"));
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
- }
-
- MixpanelAPI api = new ListeningAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "TRACKCHARGE TEST TOKEN");
- api.getPeople().identify("TRACKCHARGE PERSON");
-
- JSONObject props;
- try {
- props = new JSONObject("{'$time':'Should override', 'Orange':'Banana'}");
- } catch (JSONException e) {
- throw new RuntimeException("Can't construct fixture for trackCharge test");
- }
-
- api.getPeople().trackCharge(2.13, props);
- assertEquals(messages.size(), 1);
-
- JSONObject message = messages.get(0).getMessage();
-
- try {
- JSONObject append = message.getJSONObject("$append");
- JSONObject newTransaction = append.getJSONObject("$transactions");
- assertEquals(newTransaction.optString("Orange"), "Banana");
- assertEquals(newTransaction.optString("$time"), "Should override");
- assertEquals(newTransaction.optDouble("$amount"), 2.13, 0);
- } catch (JSONException e) {
- fail("Transaction message had unexpected layout:\n" + message.toString());
- }
+ try {
+ JSONArray unsetMessage = peopleMessage.getJSONArray("$unset");
+ assertEquals(1, unsetMessage.length());
+ assertEquals("group_key", unsetMessage.get(0));
+ } catch (JSONException e) {
+ fail("People message has an unexpected shape " + e);
}
- @Test
- public void testTrackWithSavedDistinctId(){
- final String savedDistinctID = "saved_distinct_id";
- final List messages = new ArrayList();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic() && !heard.getEventName().equals("$identify")) {
- messages.add(heard);
- }
- }
-
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- }
+ messages.clear();
+ }
+
+ @Test
+ public void testIdentifyCall() throws JSONException {
+ String newDistinctId = "New distinct ID";
+ final List messages =
+ new ArrayList();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic()) {
+ messages.add(heard);
+ }
+ }
};
- class TestMixpanelAPI extends MixpanelAPI {
- public TestMixpanelAPI(Context c, Future prefs, String token) {
- super(c, prefs, token, false, null, true);
- }
-
- @Override
- /* package */ PersistentIdentity getPersistentIdentity(final Context context, final Future referrerPreferences, final String token, final String instanceName) {
- String instanceKey = instanceName != null ? instanceName : token;
- final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
- final SharedPreferences mpSharedPrefs = context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
- mpSharedPrefs.edit().clear().putBoolean(token, true).putBoolean("has_launched", true).commit();
- final String prefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI_" + instanceKey;
- final SharedPreferences loadstorePrefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
- loadstorePrefs.edit().clear().putString("events_distinct_id", savedDistinctID).putString("people_distinct_id", savedDistinctID).commit();
- return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
+ // Track calls to the flags endpoint
+ final List flagsEndpointCalls = new ArrayList<>();
+
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Test Identify Call") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+
+ @Override
+ protected RemoteService getHttpService() {
+ // Return a mock RemoteService that tracks calls to the flags endpoint
+ return new HttpService() {
+ @Override
+ public byte[] performRequest(
+ String endpointUrl,
+ ProxyServerInteractor interactor,
+ Map params,
+ Map headers,
+ byte[] requestBodyBytes,
+ SSLSocketFactory socketFactory)
+ throws ServiceUnavailableException, IOException {
+ // Track calls to the flags endpoint
+ if (endpointUrl != null && endpointUrl.contains("/flags/")) {
+ flagsEndpointCalls.add(endpointUrl);
}
-
- @Override
- /* package */ boolean sendAppOpen() {
- return false;
- }
-
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
- }
-
- TestMixpanelAPI mpMetrics = new TestMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "SAME TOKEN");
- assertEquals(mpMetrics.getDistinctId(), savedDistinctID);
- mpMetrics.identify("new_user");
-
- mpMetrics.track("eventname", null);
- mpMetrics.getPeople().set("people prop name", "Indeed");
-
- assertEquals(2, messages.size());
-
- AnalyticsMessages.EventDescription eventMessage = (AnalyticsMessages.EventDescription) messages.get(0);
- JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(1)).getMessage();
-
- try {
- JSONObject eventProps = eventMessage.getProperties();
- String deviceId = eventProps.getString("$device_id");
- assertEquals(savedDistinctID, deviceId);
- boolean hadPersistedDistinctId = eventProps.getBoolean("$had_persisted_distinct_id");
- assertEquals(true, hadPersistedDistinctId);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
-
- try {
- String deviceId = peopleMessage.getString("$device_id");
- boolean hadPersistedDistinctId = peopleMessage.getBoolean("$had_persisted_distinct_id");
- assertEquals(savedDistinctID, deviceId);
- assertEquals(true, hadPersistedDistinctId);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
- messages.clear();
- }
-
- @Test
- public void testSetAddRemoveGroup(){
- final List messages = new ArrayList();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic() &&
- !heard.getEventName().equals("$identify") &&
- !heard.getEventName().equals("Integration")) {
- messages.add(heard);
- }
- }
-
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- }
+ // Return empty flags response
+ return "{\"flags\":{}}".getBytes();
+ }
+ };
+ }
};
- class TestMixpanelAPI extends MixpanelAPI {
- public TestMixpanelAPI(Context c, Future prefs, String token) {
- super(c, prefs, token, false, null, true);
- }
+ String oldDistinctId = metrics.getDistinctId();
- @Override
- /* package */ boolean sendAppOpen() {
- return false;
- }
+ // Clear any flags calls from constructor
+ flagsEndpointCalls.clear();
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
- }
-
- TestMixpanelAPI mpMetrics = new TestMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "SAME TOKEN");
- mpMetrics.identify("new_user");
-
- int groupID = 42;
- mpMetrics.setGroup("group_key", groupID);
- mpMetrics.track("eventname", null);
-
- assertEquals(2, messages.size());
-
- JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(0)).getMessage();
- AnalyticsMessages.EventDescription eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
-
- try {
- JSONObject eventProps = eventMessage.getProperties();
- JSONArray groupIDs = eventProps.getJSONArray("group_key");
- assertEquals((new JSONArray()).put(groupID), groupIDs);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
-
- try {
- JSONObject setMessage = peopleMessage.getJSONObject("$set");
- assertEquals((new JSONArray()).put(groupID), setMessage.getJSONArray("group_key"));
- } catch (JSONException e) {
- fail("People message has an unexpected shape " + e);
- }
-
- messages.clear();
-
- int groupID2 = 77;
- mpMetrics.addGroup("group_key", groupID2);
- mpMetrics.track("eventname", null);
- JSONArray expectedGroupIDs = new JSONArray();
- expectedGroupIDs.put(groupID);
- expectedGroupIDs.put(groupID2);
-
- assertEquals(2, messages.size());
- peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(0)).getMessage();
- eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
-
- try {
- JSONObject eventProps = eventMessage.getProperties();
- JSONArray groupIDs = eventProps.getJSONArray("group_key");
- assertEquals(expectedGroupIDs, groupIDs);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
-
- try {
- JSONObject unionMessage = peopleMessage.getJSONObject("$union");
- assertEquals((new JSONArray()).put(groupID2), unionMessage.getJSONArray("group_key"));
- } catch (JSONException e) {
- fail("People message has an unexpected shape " + e);
- }
-
- messages.clear();
- mpMetrics.removeGroup("group_key", groupID2);
- mpMetrics.track("eventname", null);
-
- assertEquals(2, messages.size());
- peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(0)).getMessage();
- eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
-
- try {
- JSONObject eventProps = eventMessage.getProperties();
- JSONArray groupIDs = eventProps.getJSONArray("group_key");
- assertEquals((new JSONArray()).put(groupID), groupIDs);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
-
- try {
- JSONObject removeMessage = peopleMessage.getJSONObject("$remove");
- assertEquals(groupID2, removeMessage.getInt("group_key"));
- } catch (JSONException e) {
- fail("People message has an unexpected shape " + e);
- }
-
- messages.clear();
- mpMetrics.removeGroup("group_key", groupID);
- mpMetrics.track("eventname", null);
-
- assertEquals(2, messages.size());
- peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(0)).getMessage();
- eventMessage = (AnalyticsMessages.EventDescription) messages.get(1);
-
- JSONObject eventProps = eventMessage.getProperties();
- assertFalse(eventProps.has("group_key"));
-
- try {
- JSONArray unsetMessage = peopleMessage.getJSONArray("$unset");
- assertEquals(1, unsetMessage.length());
- assertEquals("group_key", unsetMessage.get(0));
- } catch (JSONException e) {
- fail("People message has an unexpected shape " + e);
- }
-
- messages.clear();
- }
-
- @Test
- public void testIdentifyCall() throws JSONException {
- String newDistinctId = "New distinct ID";
- final List messages = new ArrayList();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic()) {
- messages.add(heard);
- }
- }
- };
+ // First identify should trigger loadFlags since distinctId changes
+ metrics.identify(newDistinctId);
- // Track calls to the flags endpoint
- final List flagsEndpointCalls = new ArrayList<>();
-
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Test Identify Call") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
-
- @Override
- protected RemoteService getHttpService() {
- // Return a mock RemoteService that tracks calls to the flags endpoint
- return new HttpService() {
- @Override
- public byte[] performRequest(String endpointUrl, ProxyServerInteractor interactor,
- Map params, Map headers,
- byte[] requestBodyBytes, SSLSocketFactory socketFactory)
- throws ServiceUnavailableException, IOException {
- // Track calls to the flags endpoint
- if (endpointUrl != null && endpointUrl.contains("/flags/")) {
- flagsEndpointCalls.add(endpointUrl);
- }
- // Return empty flags response
- return "{\"flags\":{}}".getBytes();
- }
- };
- }
- };
-
- String oldDistinctId = metrics.getDistinctId();
-
- // Clear any flags calls from constructor
- flagsEndpointCalls.clear();
-
- // First identify should trigger loadFlags since distinctId changes
- metrics.identify(newDistinctId);
-
- // Give the async flag loading some time to execute
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- // Ignore
- }
-
- // Second and third identify should NOT trigger loadFlags since distinctId doesn't change
- metrics.identify(newDistinctId);
- metrics.identify(newDistinctId);
-
- // Verify that only one $identify event was tracked
- assertEquals(1, messages.size());
- AnalyticsMessages.EventDescription identifyEventDescription = messages.get(0);
- assertEquals("$identify", identifyEventDescription.getEventName());
- String newDistinctIdIdentifyTrack = identifyEventDescription.getProperties().getString("distinct_id");
- String anonDistinctIdIdentifyTrack = identifyEventDescription.getProperties().getString("$anon_distinct_id");
-
- assertEquals(newDistinctId, newDistinctIdIdentifyTrack);
- assertEquals(oldDistinctId, anonDistinctIdIdentifyTrack);
-
- // Assert that loadFlags was called (flags endpoint was hit) when distinctId changed
- assertTrue("loadFlags should have been called when distinctId changed",
- flagsEndpointCalls.size() >= 1);
+ // Give the async flag loading some time to execute
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ // Ignore
}
- @Test
- public void testIdentifyResetCall() throws JSONException {
- String newDistinctId = "New distinct ID";
- final List messages = new ArrayList();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic()) {
- messages.add(heard);
- }
- }
+ // Second and third identify should NOT trigger loadFlags since distinctId doesn't change
+ metrics.identify(newDistinctId);
+ metrics.identify(newDistinctId);
+
+ // Verify that only one $identify event was tracked
+ assertEquals(1, messages.size());
+ AnalyticsMessages.EventDescription identifyEventDescription = messages.get(0);
+ assertEquals("$identify", identifyEventDescription.getEventName());
+ String newDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("distinct_id");
+ String anonDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("$anon_distinct_id");
+
+ assertEquals(newDistinctId, newDistinctIdIdentifyTrack);
+ assertEquals(oldDistinctId, anonDistinctIdIdentifyTrack);
+
+ // Assert that loadFlags was called (flags endpoint was hit) when distinctId changed
+ assertTrue(
+ "loadFlags should have been called when distinctId changed",
+ flagsEndpointCalls.size() >= 1);
+ }
+
+ @Test
+ public void testIdentifyResetCall() throws JSONException {
+ String newDistinctId = "New distinct ID";
+ final List messages =
+ new ArrayList();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic()) {
+ messages.add(heard);
+ }
+ }
};
- MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "Test Identify Call") {
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
+ MixpanelAPI metrics =
+ new TestUtils.CleanMixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "Test Identify Call") {
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
};
- ArrayList oldDistinctIds = new ArrayList<>();
- oldDistinctIds.add(metrics.getDistinctId());
- metrics.identify(newDistinctId + "0");
- metrics.reset();
-
- assertThat(oldDistinctIds, not(hasItem(metrics.getDistinctId())));
- oldDistinctIds.add(metrics.getDistinctId());
- metrics.identify(newDistinctId + "1");
- metrics.reset();
-
- assertThat(oldDistinctIds, not(hasItem(metrics.getDistinctId())));
- oldDistinctIds.add(metrics.getDistinctId());
- metrics.identify(newDistinctId + "2");
-
- assertEquals(messages.size(), 3);
- for (int i=0; i < 3; i++) {
- AnalyticsMessages.EventDescription identifyEventDescription = messages.get(i);
- assertEquals(identifyEventDescription.getEventName(), "$identify");
- String newDistinctIdIdentifyTrack = identifyEventDescription.getProperties().getString("distinct_id");
- String anonDistinctIdIdentifyTrack = identifyEventDescription.getProperties().getString("$anon_distinct_id");
-
- assertEquals(newDistinctIdIdentifyTrack, newDistinctId + i);
- assertEquals(anonDistinctIdIdentifyTrack, oldDistinctIds.get(i));
- }
+ ArrayList oldDistinctIds = new ArrayList<>();
+ oldDistinctIds.add(metrics.getDistinctId());
+ metrics.identify(newDistinctId + "0");
+ metrics.reset();
+
+ assertThat(oldDistinctIds, not(hasItem(metrics.getDistinctId())));
+ oldDistinctIds.add(metrics.getDistinctId());
+ metrics.identify(newDistinctId + "1");
+ metrics.reset();
+
+ assertThat(oldDistinctIds, not(hasItem(metrics.getDistinctId())));
+ oldDistinctIds.add(metrics.getDistinctId());
+ metrics.identify(newDistinctId + "2");
+
+ assertEquals(messages.size(), 3);
+ for (int i = 0; i < 3; i++) {
+ AnalyticsMessages.EventDescription identifyEventDescription = messages.get(i);
+ assertEquals(identifyEventDescription.getEventName(), "$identify");
+ String newDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("distinct_id");
+ String anonDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("$anon_distinct_id");
+
+ assertEquals(newDistinctIdIdentifyTrack, newDistinctId + i);
+ assertEquals(anonDistinctIdIdentifyTrack, oldDistinctIds.get(i));
+ }
+ }
+
+ @Test
+ public void testPersistence() {
+ MixpanelAPI metricsOne =
+ new MixpanelAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "SAME TOKEN",
+ false,
+ null,
+ true);
+ metricsOne.reset();
+
+ JSONObject props;
+ try {
+ props = new JSONObject("{ 'a' : 'value of a', 'b' : 'value of b' }");
+ } catch (JSONException e) {
+ throw new RuntimeException("Can't construct fixture for super properties test.");
}
- @Test
- public void testPersistence() {
- MixpanelAPI metricsOne = new MixpanelAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "SAME TOKEN", false, null, true);
- metricsOne.reset();
-
- JSONObject props;
- try {
- props = new JSONObject("{ 'a' : 'value of a', 'b' : 'value of b' }");
- } catch (JSONException e) {
- throw new RuntimeException("Can't construct fixture for super properties test.");
- }
-
- metricsOne.clearSuperProperties();
- metricsOne.registerSuperProperties(props);
- metricsOne.identify("Expected Events Identity");
-
- // We exploit the fact that any metrics object with the same token
- // will get their values from the same persistent store.
-
- final List messages = new ArrayList();
- final AnalyticsMessages listener = new AnalyticsMessages(InstrumentationRegistry.getInstrumentation().getContext(), MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
- @Override
- public void eventsMessage(EventDescription heard) {
- if (!heard.isAutomatic()) {
- messages.add(heard);
- }
- }
-
- @Override
- public void peopleMessage(PeopleDescription heard) {
- messages.add(heard);
- }
+ metricsOne.clearSuperProperties();
+ metricsOne.registerSuperProperties(props);
+ metricsOne.identify("Expected Events Identity");
+
+ // We exploit the fact that any metrics object with the same token
+ // will get their values from the same persistent store.
+
+ final List messages = new ArrayList();
+ final AnalyticsMessages listener =
+ new AnalyticsMessages(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ @Override
+ public void eventsMessage(EventDescription heard) {
+ if (!heard.isAutomatic()) {
+ messages.add(heard);
+ }
+ }
+
+ @Override
+ public void peopleMessage(PeopleDescription heard) {
+ messages.add(heard);
+ }
};
- class ListeningAPI extends MixpanelAPI {
- public ListeningAPI(Context c, Future prefs, String token) {
- super(c, prefs, token, false, null, true);
- }
-
- @Override
- /* package */ PersistentIdentity getPersistentIdentity(final Context context, final Future referrerPreferences, final String token, final String instanceName) {
- String instanceKey = instanceName != null ? instanceName : token;
- final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
- final SharedPreferences mpSharedPrefs = context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
- mpSharedPrefs.edit().clear().putBoolean(instanceKey, true).putBoolean("has_launched", true).commit();
-
- return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
- }
-
- @Override
- /* package */ boolean sendAppOpen() {
- return false;
- }
-
- @Override
- protected AnalyticsMessages getAnalyticsMessages() {
- return listener;
- }
- }
+ class ListeningAPI extends MixpanelAPI {
+ public ListeningAPI(Context c, Future prefs, String token) {
+ super(c, prefs, token, false, null, true);
+ }
+
+ @Override
+ /* package */ PersistentIdentity getPersistentIdentity(
+ final Context context,
+ final Future referrerPreferences,
+ final String token,
+ final String instanceName) {
+ String instanceKey = instanceName != null ? instanceName : token;
+ final String mixpanelPrefsName = "com.mixpanel.android.mpmetrics.Mixpanel";
+ final SharedPreferences mpSharedPrefs =
+ context.getSharedPreferences(mixpanelPrefsName, Context.MODE_PRIVATE);
+ mpSharedPrefs
+ .edit()
+ .clear()
+ .putBoolean(instanceKey, true)
+ .putBoolean("has_launched", true)
+ .commit();
+
+ return super.getPersistentIdentity(context, referrerPreferences, token, instanceName);
+ }
+
+ @Override
+ /* package */ boolean sendAppOpen() {
+ return false;
+ }
+
+ @Override
+ protected AnalyticsMessages getAnalyticsMessages() {
+ return listener;
+ }
+ }
- MixpanelAPI differentToken = new ListeningAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "DIFFERENT TOKEN");
+ MixpanelAPI differentToken =
+ new ListeningAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "DIFFERENT TOKEN");
- differentToken.track("other event", null);
- differentToken.getPeople().set("other people prop", "Word"); // should be queued up.
+ differentToken.track("other event", null);
+ differentToken.getPeople().set("other people prop", "Word"); // should be queued up.
- assertEquals(2, messages.size());
+ assertEquals(2, messages.size());
- AnalyticsMessages.EventDescription eventMessage = (AnalyticsMessages.EventDescription) messages.get(0);
+ AnalyticsMessages.EventDescription eventMessage =
+ (AnalyticsMessages.EventDescription) messages.get(0);
- try {
- JSONObject eventProps = eventMessage.getProperties();
- String sentId = eventProps.getString("distinct_id");
- String sentA = eventProps.optString("a");
- String sentB = eventProps.optString("b");
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ String sentId = eventProps.getString("distinct_id");
+ String sentA = eventProps.optString("a");
+ String sentB = eventProps.optString("b");
- assertFalse("Expected Events Identity".equals(sentId));
- assertEquals("", sentA);
- assertEquals("", sentB);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
+ assertFalse("Expected Events Identity".equals(sentId));
+ assertEquals("", sentA);
+ assertEquals("", sentB);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- messages.clear();
+ messages.clear();
- MixpanelAPI metricsTwo = new ListeningAPI(InstrumentationRegistry.getInstrumentation().getContext(), mMockPreferences, "SAME TOKEN");
+ MixpanelAPI metricsTwo =
+ new ListeningAPI(
+ InstrumentationRegistry.getInstrumentation().getContext(),
+ mMockPreferences,
+ "SAME TOKEN");
- metricsTwo.track("eventname", null);
- metricsTwo.getPeople().set("people prop name", "Indeed");
+ metricsTwo.track("eventname", null);
+ metricsTwo.getPeople().set("people prop name", "Indeed");
- assertEquals(2, messages.size());
+ assertEquals(2, messages.size());
- eventMessage = (AnalyticsMessages.EventDescription) messages.get(0);
- JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription)messages.get(1)).getMessage();
+ eventMessage = (AnalyticsMessages.EventDescription) messages.get(0);
+ JSONObject peopleMessage = ((AnalyticsMessages.PeopleDescription) messages.get(1)).getMessage();
- try {
- JSONObject eventProps = eventMessage.getProperties();
- String sentId = eventProps.getString("distinct_id");
- String sentA = eventProps.getString("a");
- String sentB = eventProps.getString("b");
+ try {
+ JSONObject eventProps = eventMessage.getProperties();
+ String sentId = eventProps.getString("distinct_id");
+ String sentA = eventProps.getString("a");
+ String sentB = eventProps.getString("b");
- assertEquals("Expected Events Identity", sentId);
- assertEquals("value of a", sentA);
- assertEquals("value of b", sentB);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape " + e);
- }
+ assertEquals("Expected Events Identity", sentId);
+ assertEquals("value of a", sentA);
+ assertEquals("value of b", sentB);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape " + e);
+ }
- try {
- String sentId = peopleMessage.getString("$distinct_id");
- assertEquals("Expected Events Identity", sentId);
- } catch (JSONException e) {
- fail("Event message has an unexpected shape: " + peopleMessage.toString());
- }
+ try {
+ String sentId = peopleMessage.getString("$distinct_id");
+ assertEquals("Expected Events Identity", sentId);
+ } catch (JSONException e) {
+ fail("Event message has an unexpected shape: " + peopleMessage.toString());
}
+ }
- @Test
- public void testTrackInThread() throws InterruptedException, JSONException {
- class TestThread extends Thread {
- final BlockingQueue mMessages;
+ @Test
+ public void testTrackInThread() throws InterruptedException, JSONException {
+ class TestThread extends Thread {
+ final BlockingQueue