package com.mixpanel.android.mpmetrics; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.mock.MockContext; import android.test.mock.MockPackageManager; import com.mixpanel.android.util.Base64Coder; import com.mixpanel.android.util.HttpService; import com.mixpanel.android.util.RemoteService; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSocketFactory; public class MixpanelBasicTest extends AndroidTestCase { @Override protected void setUp() throws Exception { super.setUp(); mMockPreferences = new TestUtils.EmptyPreferences(getContext()); AnalyticsMessages messages = AnalyticsMessages.getInstance(getContext()); messages.hardKill(); Thread.sleep(500); try { SystemInformation systemInformation = new SystemInformation(mContext); 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 public void testVersionsMatch() { assertEquals(BuildConfig.MIXPANEL_VERSION, MPConfig.VERSION); } public void testGeneratedDistinctId() { String fakeToken = UUID.randomUUID().toString(); MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, fakeToken); String generatedId1 = mixpanel.getDistinctId(); assertTrue(generatedId1 != null); mixpanel.reset(); String generatedId2 = mixpanel.getDistinctId(); assertTrue(generatedId2 != null); assertTrue(generatedId1 != generatedId2); } public void testDeleteDB() { Map<String, String> beforeMap = new HashMap<String, String>(); beforeMap.put("added", "before"); JSONObject before = new JSONObject(beforeMap); Map<String, String> afterMap = new HashMap<String,String>(); afterMap.put("added", "after"); JSONObject after = new JSONObject(afterMap); MPDbAdapter adapter = new MPDbAdapter(getContext(), "DeleteTestDB"); adapter.addJSON(before, MPDbAdapter.Table.EVENTS); adapter.addJSON(before, MPDbAdapter.Table.PEOPLE); adapter.deleteDB(); String[] emptyEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS); assertEquals(emptyEventsData, null); String[] emptyPeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE); assertEquals(emptyPeopleData, null); adapter.addJSON(after, MPDbAdapter.Table.EVENTS); adapter.addJSON(after, MPDbAdapter.Table.PEOPLE); try { String[] someEventsData = adapter.generateDataString(MPDbAdapter.Table.EVENTS); JSONArray someEvents = new JSONArray(someEventsData[1]); assertEquals(someEvents.length(), 1); assertEquals(someEvents.getJSONObject(0).get("added"), "after"); String[] somePeopleData = adapter.generateDataString(MPDbAdapter.Table.PEOPLE); JSONArray somePeople = new JSONArray(somePeopleData[1]); assertEquals(somePeople.length(), 1); assertEquals(somePeople.getJSONObject(0).get("added"), "after"); } catch (JSONException e) { fail("Unexpected JSON or lack thereof in MPDbAdapter test"); } } public void testLooperDestruction() { final BlockingQueue<JSONObject> messages = new LinkedBlockingQueue<JSONObject>(); final MPDbAdapter explodingDb = new MPDbAdapter(getContext()) { @Override public int addJSON(JSONObject message, MPDbAdapter.Table table) { messages.add(message); throw new RuntimeException("BANG!"); } }; final AnalyticsMessages explodingMessages = new AnalyticsMessages(getContext()) { // This will throw inside of our worker thread. @Override public MPDbAdapter makeDbAdapter(Context context) { return explodingDb; } }; MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "TEST TOKEN testLooperDisaster") { @Override protected AnalyticsMessages getAnalyticsMessages() { return explodingMessages; } }; try { mixpanel.reset(); assertFalse(explodingMessages.isDead()); mixpanel.track("event1", null); JSONObject found = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertNotNull(found); 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"); } } public void testEventOperations() throws JSONException { final BlockingQueue<JSONObject> messages = new LinkedBlockingQueue<JSONObject>(); final MPDbAdapter eventOperationsAdapter = new MPDbAdapter(getContext()) { @Override public int addJSON(JSONObject message, MPDbAdapter.Table table) { messages.add(message); return 1; } }; final AnalyticsMessages eventOperationsMessages = new AnalyticsMessages(getContext()) { // This will throw inside of our worker thread. @Override public MPDbAdapter makeDbAdapter(Context context) { return eventOperationsAdapter; } }; MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(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(); Map<String, Object> mapObj1 = new HashMap<>(); Map<String, Object> mapObj2 = new HashMap<>(); Map<String, Object> mapObj3 = new HashMap<>(); Map<String, Object> mapObj4 = 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"); 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"); 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")); } catch (InterruptedException e) { fail("Unexpected interruption"); } } public void testPeopleOperations() throws JSONException { final List<JSONObject> messages = new ArrayList<JSONObject>(); final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override public void peopleMessage(JSONObject heard) { messages.add(heard); } }; MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "TEST TOKEN testIdentifyAfterSet") { @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } }; Map<String, Object> mapObj1 = new HashMap<>(); mapObj1.put("SET MAP INT", 1); Map<String, Object> mapObj2 = new HashMap<>(); mapObj2.put("SET ONCE MAP STR", "SET ONCE MAP VALUE"); mixpanel.getPeople().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).getJSONObject("$set"); assertEquals("SET VALUE", setMessage.getString("SET NAME")); JSONObject setMapMessage = messages.get(1).getJSONObject("$set"); assertEquals(mapObj1.get("SET MAP INT"), setMapMessage.getInt("SET MAP INT")); JSONObject addMessage = messages.get(2).getJSONObject("$add"); assertEquals(1, addMessage.getInt("INCREMENT NAME")); JSONObject appendMessage = messages.get(3).getJSONObject("$append"); assertEquals("APPEND VALUE", appendMessage.get("APPEND NAME")); JSONObject setOnceMessage = messages.get(4).getJSONObject("$set_once"); assertEquals("SET ONCE VALUE", setOnceMessage.getString("SET ONCE NAME")); JSONObject setOnceMapMessage = messages.get(5).getJSONObject("$set_once"); assertEquals(mapObj2.get("SET ONCE MAP STR"), setOnceMapMessage.getString("SET ONCE MAP STR")); JSONObject unionMessage = messages.get(6).getJSONObject("$union"); JSONArray unionValues = unionMessage.getJSONArray("UNION NAME"); assertEquals(1, unionValues.length()); assertEquals(100, unionValues.getInt(0)); JSONArray unsetMessage = messages.get(7).getJSONArray("$unset"); assertEquals(1, unsetMessage.length()); assertEquals("UNSET NAME", unsetMessage.get(0)); JSONObject trackChargeMessage = messages.get(8).getJSONObject("$append"); JSONObject transaction = trackChargeMessage.getJSONObject("$transactions"); assertEquals(100.0d, transaction.getDouble("$amount")); JSONArray clearChargesMessage = messages.get(9).getJSONArray("$unset"); assertEquals(1, clearChargesMessage.length()); assertEquals("$transactions", clearChargesMessage.getString(0)); assertTrue(messages.get(10).has("$delete")); } public void testIdentifyAfterSet() { final List<JSONObject> messages = new ArrayList<JSONObject>(); final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override public void peopleMessage(JSONObject heard) { messages.add(heard); } }; MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "TEST TOKEN testIdentifyAfterSet") { @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.increment("the prop", 4); people.append("the prop", 5); people.append("the prop", 6); people.identify("Personal Identity"); assertEquals(messages.size(), 7); try { for (JSONObject message: messages) { String distinctId = message.getString("$distinct_id"); assertEquals(distinctId, "Personal Identity"); } assertTrue(messages.get(0).has("$add")); assertTrue(messages.get(1).has("$append")); assertTrue(messages.get(2).has("$set")); assertTrue(messages.get(3).has("$add")); assertTrue(messages.get(4).has("$add")); } catch (JSONException e) { fail("Unexpected JSON error in stored messages."); } } public void testIdentifyAndGetDistinctId() { MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "Identify Test Token"); String generatedId = metrics.getDistinctId(); assertNotNull(generatedId); String emptyId = metrics.getPeople().getDistinctId(); assertNull(emptyId); metrics.identify("Events Id"); String setId = metrics.getDistinctId(); assertEquals("Events Id", setId); String stillEmpty = metrics.getPeople().getDistinctId(); assertNull(stillEmpty); metrics.getPeople().identify("People Id"); String unchangedId = metrics.getDistinctId(); assertEquals("Events Id", unchangedId); String setPeopleId = metrics.getPeople().getDistinctId(); assertEquals("People Id", setPeopleId); } public void testMessageQueuing() { final BlockingQueue<String> messages = new LinkedBlockingQueue<String>(); final SynchronizedReference<Boolean> isIdentifiedRef = new SynchronizedReference<Boolean>(); isIdentifiedRef.set(false); final MPDbAdapter mockAdapter = new MPDbAdapter(getContext()) { @Override public int addJSON(JSONObject message, MPDbAdapter.Table table) { try { messages.put("TABLE " + table.getName()); messages.put(message.toString()); } catch (InterruptedException e) { throw new RuntimeException(e); } return super.addJSON(message, 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(String endpointUrl, Map<String, Object> params, SSLSocketFactory socketFactory) { final boolean isIdentified = isIdentifiedRef.get(); if (null == params) { if (isIdentified) { assertEquals("DECIDE_ENDPOINT?version=1&lib=android&token=Test+Message+Queuing&distinct_id=PEOPLE+ID" + mAppProperties, endpointUrl); } else { assertEquals("DECIDE_ENDPOINT?version=1&lib=android&token=Test+Message+Queuing&distinct_id=EVENTS+ID" + mAppProperties, endpointUrl); } return TestUtils.bytes("{}"); } 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"); } }; final MPConfig mockConfig = new MPConfig(new Bundle(), getContext()) { @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 getDecideEndpoint() { return "DECIDE_ENDPOINT"; } @Override public boolean getDisableAppOpenEvent() { return true; } }; final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override protected MPDbAdapter makeDbAdapter(Context context) { return mockAdapter; } @Override protected MPConfig getConfig(Context context) { return mockConfig; } @Override protected RemoteService getPoster() { return mockPoster; } }; MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "Test Message Queuing") { @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } }; metrics.identify("EVENTS ID"); // Test filling up the message queue for (int i=0; i < mockConfig.getBulkUploadLimit() - 1; i++) { metrics.track("frequent event", null); } metrics.track("final event", null); String expectedJSONMessage = "<No message actually received>"; try { for (int i=0; i < mockConfig.getBulkUploadLimit() - 1; i++) { String messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONObject message = new JSONObject(expectedJSONMessage); assertEquals("frequent event", message.getString("event")); } String messageTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), messageTable); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONObject 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); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONArray bigFlush = new JSONArray(expectedJSONMessage); assertEquals(mockConfig.getBulkUploadLimit(), bigFlush.length()); metrics.track("next wave", null); metrics.flush(); String nextWaveTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("TABLE " + MPDbAdapter.Table.EVENTS.getName(), nextWaveTable); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONObject nextWaveMessage = new JSONObject(expectedJSONMessage); assertEquals("next wave", nextWaveMessage.getString("event")); String manualFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("SENT FLUSH EVENTS_ENDPOINT", manualFlush); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONArray nextWave = new JSONArray(expectedJSONMessage); assertEquals(1, nextWave.length()); JSONObject nextWaveEvent = nextWave.getJSONObject(0); assertEquals("next wave", nextWaveEvent.getString("event")); isIdentifiedRef.set(true); metrics.getPeople().identify("PEOPLE ID"); metrics.getPeople().set("prop", "yup"); metrics.flush(); String peopleTable = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("TABLE " + MPDbAdapter.Table.PEOPLE.getName(), peopleTable); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONObject peopleMessage = new JSONObject(expectedJSONMessage); assertEquals("PEOPLE ID", peopleMessage.getString("$distinct_id")); assertEquals("yup", peopleMessage.getJSONObject("$set").getString("prop")); String peopleFlush = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertEquals("SENT FLUSH PEOPLE_ENDPOINT", peopleFlush); expectedJSONMessage = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); JSONArray peopleSent = new JSONArray(expectedJSONMessage); assertEquals(1, peopleSent.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); } } public void testTrackCharge() { final List<JSONObject> messages = new ArrayList<JSONObject>(); final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override public void eventsMessage(EventDescription heard) { throw new RuntimeException("Should not be called during this test"); } @Override public void peopleMessage(JSONObject heard) { messages.add(heard); } }; class ListeningAPI extends TestUtils.CleanMixpanelAPI { public ListeningAPI(Context c, Future<SharedPreferences> referrerPrefs, String token) { super(c, referrerPrefs, token); } @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } } MixpanelAPI api = new ListeningAPI(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); 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); } catch (JSONException e) { fail("Transaction message had unexpected layout:\n" + message.toString()); } } public void testPersistence() { MixpanelAPI metricsOne = new MixpanelAPI(getContext(), mMockPreferences, "SAME TOKEN"); 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"); metricsOne.getPeople().identify("Expected People Identity"); // We exploit the fact that any metrics object with the same token // will get their values from the same persistent store. final List<Object> messages = new ArrayList<Object>(); final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override public void eventsMessage(EventDescription heard) { messages.add(heard); } @Override public void peopleMessage(JSONObject heard) { messages.add(heard); } }; class ListeningAPI extends MixpanelAPI { public ListeningAPI(Context c, Future<SharedPreferences> prefs, String token) { super(c, prefs, token); } @Override /* package */ boolean sendAppOpen() { return false; } @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } } MixpanelAPI differentToken = new ListeningAPI(getContext(), mMockPreferences, "DIFFERENT TOKEN"); differentToken.track("other event", null); differentToken.getPeople().set("other people prop", "Word"); // should be queued up. assertEquals(2, messages.size()); // track integration 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"); assertFalse("Expected Events Identity".equals(sentId)); assertEquals("", sentA); assertEquals("", sentB); } catch (JSONException e) { fail("Event message has an unexpected shape " + e); } messages.clear(); MixpanelAPI metricsTwo = new ListeningAPI(getContext(), mMockPreferences, "SAME TOKEN"); metricsTwo.track("eventname", null); metricsTwo.getPeople().set("people prop name", "Indeed"); assertEquals(3, messages.size()); eventMessage = (AnalyticsMessages.EventDescription) messages.get(1); JSONObject peopleMessage = (JSONObject) messages.get(2); 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); } try { String sentId = peopleMessage.getString("$distinct_id"); assertEquals("Expected People Identity", sentId); } catch (JSONException e) { fail("Event message has an unexpected shape: " + peopleMessage.toString()); } } public void testTrackInThread() throws InterruptedException, JSONException { class TestThread extends Thread { BlockingQueue<JSONObject> mMessages; public TestThread(BlockingQueue<JSONObject> messages) { this.mMessages = messages; } @Override public void run() { final MPDbAdapter dbMock = new MPDbAdapter(getContext()) { @Override public int addJSON(JSONObject message, MPDbAdapter.Table table) { mMessages.add(message); return 1; } }; final AnalyticsMessages analyticsMessages = new AnalyticsMessages(getContext()) { @Override public MPDbAdapter makeDbAdapter(Context context) { return dbMock; } }; MixpanelAPI mixpanel = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "TEST TOKEN") { @Override protected AnalyticsMessages getAnalyticsMessages() { return analyticsMessages; } }; mixpanel.reset(); mixpanel.track("test in thread", new JSONObject()); } } ////////////////////////////// final BlockingQueue<JSONObject> messages = new LinkedBlockingQueue<JSONObject>(); TestThread testThread = new TestThread(messages); testThread.start(); JSONObject found = messages.poll(POLL_WAIT_SECONDS, TimeUnit.SECONDS); assertNotNull(found); assertEquals(found.getString("event"), "test in thread"); assertTrue(found.getJSONObject("properties").has("$bluetooth_version")); } public void testConfiguration() { final ApplicationInfo appInfo = new ApplicationInfo(); appInfo.metaData = new Bundle(); appInfo.metaData.putInt("com.mixpanel.android.MPConfig.BulkUploadLimit", 1); appInfo.metaData.putInt("com.mixpanel.android.MPConfig.FlushInterval", 2); appInfo.metaData.putInt("com.mixpanel.android.MPConfig.DataExpiration", 3); appInfo.metaData.putBoolean("com.mixpanel.android.MPConfig.DisableFallback", true); appInfo.metaData.putBoolean("com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates", false); appInfo.metaData.putBoolean("com.mixpanel.android.MPConfig.DisableGestureBindingUI", true); appInfo.metaData.putBoolean("com.mixpanel.android.MPConfig.DisableEmulatorBindingUI", true); appInfo.metaData.putBoolean("com.mixpanel.android.MPConfig.DisableAppOpenEvent", true); appInfo.metaData.putString("com.mixpanel.android.MPConfig.EventsEndpoint", "EVENTS ENDPOINT"); appInfo.metaData.putString("com.mixpanel.android.MPConfig.EventsFallbackEndpoint", "EVENTS FALLBACK ENDPOINT"); appInfo.metaData.putString("com.mixpanel.android.MPConfig.PeopleEndpoint", "PEOPLE ENDPOINT"); appInfo.metaData.putString("com.mixpanel.android.MPConfig.PeopleFallbackEndpoint", "PEOPLE FALLBACK ENDPOINT"); appInfo.metaData.putString("com.mixpanel.android.MPConfig.DecideEndpoint", "DECIDE ENDPOINT"); appInfo.metaData.putString("com.mixpanel.android.MPConfig.DecideFallbackEndpoint", "DECIDE FALLBACK ENDPOINT"); final PackageManager packageManager = new MockPackageManager() { @Override public ApplicationInfo getApplicationInfo(String packageName, int flags) { assertEquals(packageName, "TEST PACKAGE NAME"); assertTrue((flags & PackageManager.GET_META_DATA) == PackageManager.GET_META_DATA); return appInfo; } }; final Context context = new MockContext() { @Override public String getPackageName() { return "TEST PACKAGE NAME"; } @Override public PackageManager getPackageManager() { return packageManager; } }; final MPConfig testConfig = MPConfig.readConfig(context); assertEquals(1, testConfig.getBulkUploadLimit()); assertEquals(2, testConfig.getFlushInterval()); assertEquals(3, testConfig.getDataExpiration()); assertEquals(true, testConfig.getDisableFallback()); assertEquals(true, testConfig.getDisableEmulatorBindingUI()); assertEquals(true, testConfig.getDisableGestureBindingUI()); assertEquals(true, testConfig.getDisableAppOpenEvent()); assertEquals(false, testConfig.getAutoShowMixpanelUpdates()); assertEquals("EVENTS ENDPOINT", testConfig.getEventsEndpoint()); assertEquals("EVENTS FALLBACK ENDPOINT", testConfig.getEventsFallbackEndpoint()); assertEquals("PEOPLE ENDPOINT", testConfig.getPeopleEndpoint()); assertEquals("PEOPLE FALLBACK ENDPOINT", testConfig.getPeopleFallbackEndpoint()); assertEquals("DECIDE ENDPOINT", testConfig.getDecideEndpoint()); assertEquals("DECIDE FALLBACK ENDPOINT", testConfig.getDecideFallbackEndpoint()); } public void test2XUrls() { final String twoXBalok = InAppNotification.sizeSuffixUrl("http://images.mxpnl.com/112690/1392337640909.49573.Balok_first.jpg", "@BANANAS"); assertEquals(twoXBalok, "http://images.mxpnl.com/112690/1392337640909.49573.Balok_first@BANANAS.jpg"); final String nothingMatches = InAppNotification.sizeSuffixUrl("http://images.mxpnl.com/112690/1392337640909.49573.Balok_first..", "@BANANAS"); assertEquals(nothingMatches, "http://images.mxpnl.com/112690/1392337640909.49573.Balok_first.."); final String emptyMatch = InAppNotification.sizeSuffixUrl("", "@BANANAS"); assertEquals(emptyMatch, ""); final String nothingExtensionful = InAppNotification.sizeSuffixUrl("http://images.mxpnl.com/112690/", "@BANANAS"); assertEquals(nothingExtensionful, "http://images.mxpnl.com/112690/"); } public void testAlias() { final RemoteService mockPoster = new HttpService() { @Override public byte[] performRequest(String endpointUrl, Map<String, Object> params, SSLSocketFactory socketFactory) { try { assertTrue(params.containsKey("data")); final String jsonData = Base64Coder.decodeString(params.get("data").toString()); JSONArray msg = new JSONArray(jsonData); JSONObject event = msg.getJSONObject(0); JSONObject properties = event.getJSONObject("properties"); assertEquals(event.getString("event"), "$create_alias"); assertEquals(properties.getString("distinct_id"), "old id"); assertEquals(properties.getString("alias"), "new id"); } catch (JSONException e) { throw new RuntimeException("Malformed data passed to test mock", e); } return TestUtils.bytes("1\n"); } }; final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override protected RemoteService getPoster() { return mockPoster; } }; MixpanelAPI metrics = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "Test Message Queuing") { @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } }; // Check that we post the alias immediately metrics.identify("old id"); metrics.alias("new id", "old id"); } private Future<SharedPreferences> mMockPreferences; private static final int POLL_WAIT_SECONDS = 10; private String mAppProperties; }