Remove A/B testing

pull/14/head
Alex Baker 11 years ago
parent 997a4725ec
commit 2866dfaa38

@ -21,7 +21,6 @@ import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.User;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.abtesting.ABTestEventReportingService;
import com.todoroo.astrid.tags.reusable.FeaturedListFilterExposer;
/**
@ -40,8 +39,6 @@ public final class ActFmSyncService {
private ActFmPreferenceService actFmPreferenceService;
@Autowired
private ActFmInvoker actFmInvoker;
@Autowired
private ABTestEventReportingService abTestEventReportingService;
private String token;
@ -49,17 +46,6 @@ public final class ActFmSyncService {
DependencyInjectionService.getInstance().inject(this);
}
private void addAbTestEventInfo(List<Object> params) {
JSONArray abTestInfo = abTestEventReportingService.getTestsWithVariantsArray();
try {
for (int i = 0; i < abTestInfo.length(); i++) {
params.add("ab_variants[]"); params.add(abTestInfo.getString(i));
}
} catch (JSONException e) {
Log.e("Error parsing AB test info", abTestInfo.toString(), e);
}
}
// --- data fetch methods
public int fetchFeaturedLists(int serverTime) throws JSONException, IOException {
if (!checkForToken()) {

@ -58,7 +58,6 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.people.PeopleFilterMode;
import com.todoroo.astrid.people.PersonViewFragment;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.service.abtesting.ABTestEventReportingService;
import com.todoroo.astrid.tags.TagFilterExposer;
import com.todoroo.astrid.tags.TagsPlugin;
import com.todoroo.astrid.tags.reusable.FeaturedListFilterMode;
@ -96,8 +95,6 @@ public class TaskListActivity extends AstridActivity implements MainMenuListener
public static final int REQUEST_CODE_RESTART = 10;
@Autowired private ABTestEventReportingService abTestEventReportingService;
@Autowired private TagMetadataDao tagMetadataDao;
private View listsNav;
@ -216,10 +213,6 @@ public class TaskListActivity extends AstridActivity implements MainMenuListener
if (getIntent().hasExtra(TOKEN_SOURCE)) {
trackActivitySource();
}
// Have to call this here because sometimes StartupService
// isn't called (i.e. if the app was silently alive in the background)
abTestEventReportingService.trackUserRetention(this);
}
private void setupPagerAdapter() {

@ -1,128 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.dao;
import com.todoroo.andlib.data.DatabaseDao;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.ABTestEvent;
public class ABTestEventDao extends DatabaseDao<ABTestEvent> {
@Autowired
private Database database;
public ABTestEventDao() {
super(ABTestEvent.class);
DependencyInjectionService.getInstance().inject(this);
setDatabase(database);
}
/**
* Creates the baseline +0 days event (i.e. the first time the user
* launches the app containing this test)
* @param testName - the name of the test
* @param testVariant - which option was chosen for this user
* @param newUser - are they a new user?
* @param activeUser - are they an activated user?
*/
public void createInitialTestEvent(String testName, String testVariant,
boolean newUser, boolean activeUser) {
ABTestEvent event = new ABTestEvent();
event.setValue(ABTestEvent.TEST_NAME, testName);
event.setValue(ABTestEvent.TEST_VARIANT, testVariant);
event.setValue(ABTestEvent.NEW_USER, newUser ? 1 : 0);
event.setValue(ABTestEvent.ACTIVATED_USER, activeUser ? 1 : 0);
event.setValue(ABTestEvent.TIME_INTERVAL, 0);
event.setValue(ABTestEvent.DATE_RECORDED, DateUtilities.now());
createNew(event);
}
/**
* Only public for unit testing--don't use unless you really mean it!
*
* Creates data points for the specified test name, creating one data point
* for each time interval that hasn't yet been recorded up to the specified one
* @param testName
* @param timeInterval
*/
public void createTestEventWithTimeInterval(String testName, int timeInterval) {
TodorooCursor<ABTestEvent> existing = query(Query.select(ABTestEvent.PROPERTIES)
.where(ABTestEvent.TEST_NAME.eq(testName)).orderBy(Order.asc(ABTestEvent.TIME_INTERVAL)));
try {
if (existing.getCount() == 0) {
return;
}
existing.moveToLast();
ABTestEvent item = new ABTestEvent(existing);
int lastRecordedTimeIntervalIndex = AndroidUtilities.indexOf(
ABTestEvent.TIME_INTERVALS, item.getValue(ABTestEvent.TIME_INTERVAL));
int currentTimeIntervalIndex = AndroidUtilities.indexOf(
ABTestEvent.TIME_INTERVALS, timeInterval);
if (lastRecordedTimeIntervalIndex < 0 || currentTimeIntervalIndex < 0) {
return;
}
long now = DateUtilities.now();
for (int i = lastRecordedTimeIntervalIndex + 1; i <= currentTimeIntervalIndex; i++) {
item.clearValue(ABTestEvent.ID);
item.setValue(ABTestEvent.REPORTED, 0);
item.setValue(ABTestEvent.TIME_INTERVAL, ABTestEvent.TIME_INTERVALS[i]);
item.setValue(ABTestEvent.DATE_RECORDED, now);
createNew(item);
}
} finally {
existing.close();
}
return;
}
/**
* For each baseline data point that exists in the database, check the current
* time against the time that baseline was recorded and report the appropriate
* +n days events. Called on startup.
*/
public void createRelativeDateEvents() {
TodorooCursor<ABTestEvent> allEvents = query(Query.select(ABTestEvent.TEST_NAME, ABTestEvent.DATE_RECORDED)
.where(ABTestEvent.TIME_INTERVAL.eq(0)));
try {
long now = DateUtilities.now();
ABTestEvent event = new ABTestEvent();
for (allEvents.moveToFirst(); !allEvents.isAfterLast(); allEvents.moveToNext()) {
event.readFromCursor(allEvents);
long baseTime = event.getValue(ABTestEvent.DATE_RECORDED);
long timeSinceBase = now - baseTime;
String testName = event.getValue(ABTestEvent.TEST_NAME);
int timeInterval = -1;
long days = timeSinceBase / DateUtilities.ONE_DAY;
for(int i = 0; i < ABTestEvent.TIME_INTERVALS.length; i++) {
if (days >= ABTestEvent.TIME_INTERVALS[i]) {
timeInterval = ABTestEvent.TIME_INTERVALS[i];
}
}
if (timeInterval > 0) {
createTestEventWithTimeInterval(testName, timeInterval);
}
}
} finally {
allEvents.close();
}
}
}

@ -14,7 +14,6 @@ import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Table;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.astrid.data.ABTestEvent;
import com.todoroo.astrid.data.History;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
@ -68,7 +67,6 @@ public class Database extends AbstractDatabase {
Update.TABLE,
User.TABLE,
UserActivity.TABLE,
ABTestEvent.TABLE,
TagMetadata.TABLE,
History.TABLE,
TaskAttachment.TABLE,
@ -316,12 +314,7 @@ public class Database extends AbstractDatabase {
Log.e("astrid", "db-upgrade-" + oldVersion + "-" + newVersion, e);
}
case 23: try {
database.execSQL(createTableSql(visitor, ABTestEvent.TABLE.name, ABTestEvent.PROPERTIES));
} catch (SQLiteException e) {
Log.e("astrid", "db-upgrade-" + oldVersion + "-" + newVersion, e);
}
case 23:
case 24: try {
database.execSQL("ALTER TABLE " + Task.TABLE.name + " ADD " +
Task.REPEAT_UNTIL.accept(visitor, null));

@ -1,135 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.data;
import android.content.ContentValues;
import android.net.Uri;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.andlib.data.Table;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.api.AstridApiConstants;
/**
* Model for data points used for tracking user retention in AB tests
* @author Sam
*
*/
public class ABTestEvent extends AbstractModel {
public static final int[] TIME_INTERVALS = { 0, 1, 3, 7, 14, 21 };
// --- table and uri
/** table for this model */
public static final Table TABLE = new Table("abtestevent", ABTestEvent.class);
/** content uri for this model */
public static final Uri CONTENT_URI = Uri.parse("content://" + AstridApiConstants.API_PACKAGE + "/" +
TABLE.name);
// --- properties
/** ID */
public static final LongProperty ID = new LongProperty(
TABLE, ID_PROPERTY_NAME);
/** Name of the test -- one of the constant test keys defined in ABOptions */
public static final StringProperty TEST_NAME = new StringProperty(
TABLE, "testName");
/**
* Which variant (option) was chosen for this user --
* one of the constants in the corresponding descriptions array in ABOptions
*/
public static final StringProperty TEST_VARIANT = new StringProperty(
TABLE, "testVariant");
/**
* Indicates if the user should be considered a new user for the purposes
* of this test.
* Should be 0 if no, 1 if yes
*/
public static final IntegerProperty NEW_USER = new IntegerProperty(
TABLE, "newUser"); // 0 if no, 1 if yes
/**
* Indicates if the user was "activated" at the time of recording this data
* point.
* Should be 0 if no, 1 if yes
* Activated: 3 tasks created, one completed
*/
public static final IntegerProperty ACTIVATED_USER = new IntegerProperty(
TABLE, "activatedUser");
/**
* Which time interval event this data point corresponds to.
* Should be one of the time interval constants defined int the
* above array.
*/
public static final IntegerProperty TIME_INTERVAL = new IntegerProperty(
TABLE, "timeInterval"); // one of the constants defined above
/** The actual date on which this data point was recorded. */
public static final LongProperty DATE_RECORDED = new LongProperty(
TABLE, "dateRecorded");
/** Whether or not this data point has been reported to the server */
public static final IntegerProperty REPORTED = new IntegerProperty(
TABLE, "reported"); // 0 if not yet reported, 1 if reported successfully
/** List of all properties for this model */
public static final Property<?>[] PROPERTIES = generateProperties(ABTestEvent.class);
private static final ContentValues defaultValues = new ContentValues();
static {
// initialize with default values
defaultValues.put(REPORTED.name, 0);
}
@Override
public ContentValues getDefaultValues() {
return defaultValues;
}
// --- data access boilerplate
public ABTestEvent() {
super();
}
public ABTestEvent(TodorooCursor<ABTestEvent> cursor) {
this();
readPropertiesFromCursor(cursor);
}
public void readFromCursor(TodorooCursor<ABTestEvent> cursor) {
super.readPropertiesFromCursor(cursor);
}
@Override
public long getId() {
return getIdHelper(ID);
}
// --- parcelable helpers
public static final Creator<ABTestEvent> CREATOR = new ModelCreator<ABTestEvent>(ABTestEvent.class);
@Override
protected Creator<? extends AbstractModel> getCreator() {
return CREATOR;
}
}

@ -13,7 +13,6 @@ import com.todoroo.andlib.service.HttpRestClient;
import com.todoroo.astrid.actfm.sync.ActFmInvoker;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.actfm.sync.ActFmSyncService;
import com.todoroo.astrid.dao.ABTestEventDao;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.HistoryDao;
import com.todoroo.astrid.dao.MetadataDao;
@ -36,10 +35,6 @@ import com.todoroo.astrid.gtasks.GtasksMetadataService;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.service.abtesting.ABChooser;
import com.todoroo.astrid.service.abtesting.ABTestEventReportingService;
import com.todoroo.astrid.service.abtesting.ABTestInvoker;
import com.todoroo.astrid.service.abtesting.ABTests;
import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.utility.Constants;
@ -118,13 +113,6 @@ public class AstridDependencyInjector extends AbstractDependencyInjector {
injectables.put("gtasksTaskListUpdater", GtasksTaskListUpdater.class);
injectables.put("gtasksSyncService", GtasksSyncService.class);
// AB testing
injectables.put("abChooser", ABChooser.class);
injectables.put("abTests", new ABTests());
injectables.put("abTestEventDao", ABTestEventDao.class);
injectables.put("abTestEventReportingService", ABTestEventReportingService.class);
injectables.put("abTestInvoker", ABTestInvoker.class);
// com.todoroo.astrid.tags
injectables.put("tagService", TagService.class);

@ -63,9 +63,6 @@ import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.opencrx.OpencrxCoreUtils;
import com.todoroo.astrid.reminders.ReengagementService;
import com.todoroo.astrid.reminders.ReminderStartupReceiver;
import com.todoroo.astrid.service.abtesting.ABChooser;
import com.todoroo.astrid.service.abtesting.ABTestInvoker;
import com.todoroo.astrid.service.abtesting.ABTests;
import com.todoroo.astrid.subtasks.SubtasksMetadata;
import com.todoroo.astrid.tags.TaskToTagMetadata;
import com.todoroo.astrid.ui.TaskListFragmentPager;
@ -118,12 +115,6 @@ public class StartupService {
@Autowired GtasksSyncService gtasksSyncService;
@Autowired ABChooser abChooser;
@Autowired ABTests abTests;
@Autowired ABTestInvoker abTestInvoker;
/**
* bit to prevent multiple initializations
*/
@ -212,12 +203,6 @@ public class StartupService {
final int finalLatestVersion = latestSetVersion;
abTests.externalInit(context);
abChooser.makeChoicesForAllTests(latestSetVersion == 0, taskService.getUserActivationStatus());
abTestInvoker.reportAcquisition();
initializeDatabaseListeners();
ActFmSyncThread.initializeSyncComponents(taskDao, tagDataDao, userActivityDao, taskAttachmentDao, taskListMetadataDao);

@ -39,7 +39,6 @@ import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.helper.DueDateTimeMigrator;
import com.todoroo.astrid.service.abtesting.ABChooser;
import com.todoroo.astrid.subtasks.SubtasksMetadataMigration;
import com.todoroo.astrid.tags.TagCaseMigrator;
import com.todoroo.astrid.utility.AstridPreferences;
@ -148,8 +147,6 @@ public final class UpgradeService {
@Autowired GtasksPreferenceService gtasksPreferenceService;
@Autowired ABChooser abChooser;
@Autowired AddOnService addOnService;
@Autowired ActFmPreferenceService actFmPreferenceService;

@ -1,117 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service.abtesting;
import java.util.Random;
import java.util.Set;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.ABTestEventDao;
/**
* Helper class to facilitate A/B testing by randomly choosing an option
* based on probabilities that can be supplied from local defaults
* @author Sam Bosley <sam@astrid.com>
*
*/
public class ABChooser {
public static final int NO_OPTION = -1;
@Autowired
private ABTests abTests;
@Autowired
private ABTestEventDao abTestEventDao;
private final Random random;
public ABChooser() {
DependencyInjectionService.getInstance().inject(this);
random = new Random();
}
/**
* Iterates through the list of all available tests and makes sure that a choice
* is made for each of them
*/
public void makeChoicesForAllTests(boolean newUser, boolean activatedUser) {
Set<String> tests = abTests.getAllTestKeys();
for (String test : tests) {
makeChoiceForTest(test, newUser, activatedUser);
}
}
/**
* If a choice/variant has not yet been selected for the specified test,
* make one and create the initial +0 analytics data point
*
* @param testKey - the preference key string of the option (defined in ABTests)
*/
private void makeChoiceForTest(String testKey, boolean newUser, boolean activatedUser) {
int pref = readChoiceForTest(testKey);
if (pref > NO_OPTION) {
return;
}
int chosen = NO_OPTION;
if (abTests.isValidTestKey(testKey)) {
int[] optionProbs = abTests.getProbsForTestKey(testKey, newUser);
String[] optionDescriptions = abTests.getDescriptionsForTestKey(testKey);
chosen = chooseOption(optionProbs);
setChoiceForTest(testKey, chosen);
String desc = optionDescriptions[chosen];
abTestEventDao.createInitialTestEvent(testKey, desc, newUser, activatedUser);
}
return;
}
/**
* Returns the chosen option if set or NO_OPTION if unset
* @param testKey
* @return
*/
public static int readChoiceForTest(String testKey) {
return Preferences.getInt(testKey, NO_OPTION);
}
/**
* Changes the choice of an A/B feature in the preferences. Useful for
* the feature flipper (can manually override previous choice)
* @param testKey
* @param choiceIndex
*/
public void setChoiceForTest(String testKey, int choiceIndex) {
if (abTests.isValidTestKey(testKey)) {
Preferences.setInt(testKey, choiceIndex);
}
}
/*
* Helper method to choose an option from an int[] corresponding to the
* relative weights of each option. Returns the index of the chosen option.
*/
private int chooseOption(int[] optionProbs) {
int sum = 0;
for (int opt : optionProbs) // Compute sum
{
sum += opt;
}
double rand = random.nextDouble() * sum; // Get uniformly distributed double between [0, sum)
sum = 0;
for (int i = 0; i < optionProbs.length; i++) {
sum += optionProbs[i];
if (rand <= sum) {
return i;
}
}
return optionProbs.length - 1;
}
}

@ -1,174 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service.abtesting;
import java.io.IOException;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.ABTestEventDao;
import com.todoroo.astrid.data.ABTestEvent;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.TaskService;
/**
* Service to manage the reporting of launch events for AB testing.
* On startup, queries the ABTestEvent database for unreported data
* points, merges them into the expected JSONArray format, and
* pushes them to the server.
* @author Sam
*
*/
public final class ABTestEventReportingService {
private static final String KEY_TEST = "test";
private static final String KEY_VARIANT = "variant";
private static final String KEY_NEW_USER = "new";
private static final String KEY_ACTIVE_USER = "activated";
private static final String KEY_DAYS = "days";
private static final String KEY_DATE = "date";
private static final String KEY_INITIAL = "initial";
private static final String KEY_ACTIVATION = "activation";
private static final String PREF_REPORTED_ACTIVATION = "p_reported_activation";
@Autowired
private ABTestEventDao abTestEventDao;
@Autowired
private ABTestInvoker abTestInvoker;
@Autowired
private ABTests abTests;
@Autowired
private TaskService taskService;
public ABTestEventReportingService() {
DependencyInjectionService.getInstance().inject(this);
}
/**
* Called on startup from TaskListActivity. Creates any +n days
* launch events that need to be recorded, and pushes all unreported
* data to the server.
*/
public void trackUserRetention(final Context context) {
new Thread(new Runnable() {
@Override
public void run() {
try {
abTestEventDao.createRelativeDateEvents();
pushAllUnreportedABTestEvents();
// reportUserActivation();
} catch (SQLiteException e) {
StartupService.handleSQLiteError(context, e);
}
}
}).start();
}
private void pushAllUnreportedABTestEvents() {
synchronized(ABTestEventReportingService.class) {
}
}
private void reportUserActivation() {
synchronized (ABTestEventReportingService.class) {
}
}
public JSONArray getTestsWithVariantsArray() {
JSONArray array = new JSONArray();
Set<String> tests = abTests.getAllTestKeys();
for (String key : tests) {
array.put(key + ":" + abTests.getDescriptionForTestOption(key, ABChooser.readChoiceForTest(key)));
}
return array;
}
private void handleException(Exception e) {
Log.e("analytics", "analytics-error", e);
}
private static JSONObject jsonFromABTestEvent(ABTestEvent model) throws JSONException {
JSONObject payload = new JSONObject();
payload.put(KEY_TEST, model.getValue(ABTestEvent.TEST_NAME));
payload.put(KEY_VARIANT, model.getValue(ABTestEvent.TEST_VARIANT));
payload.put(KEY_NEW_USER, model.getValue(ABTestEvent.NEW_USER) > 0 ? true : false);
payload.put(KEY_ACTIVE_USER, model.getValue(ABTestEvent.ACTIVATED_USER) > 0 ? true : false);
payload.put(KEY_DAYS, new JSONArray().put(model.getValue(ABTestEvent.TIME_INTERVAL)));
long date = model.getValue(ABTestEvent.DATE_RECORDED) / 1000L;
payload.put(KEY_DATE, date);
return payload;
}
private static JSONArray jsonArrayFromABTestEvents(TodorooCursor<ABTestEvent> events) throws JSONException {
JSONArray result = new JSONArray();
String lastTestKey = "";
JSONObject testAcc = null;
ABTestEvent model = new ABTestEvent();
for (events.moveToFirst(); !events.isAfterLast(); events.moveToNext()) {
model.readFromCursor(events);
if (!model.getValue(ABTestEvent.TEST_NAME).equals(lastTestKey)) {
if (testAcc != null) {
result.put(testAcc);
}
testAcc = jsonFromABTestEvent(model);
lastTestKey = model.getValue(ABTestEvent.TEST_NAME);
} else {
int interval = model.getValue(ABTestEvent.TIME_INTERVAL);
if (testAcc != null) { // this should never happen, just stopping the compiler from complaining
JSONArray daysArray = testAcc.getJSONArray(KEY_DAYS);
daysArray.put(interval);
}
}
}
if (testAcc != null) {
result.put(testAcc);
}
return result;
}
private static JSONArray jsonArrayForActivationAnalytics(TodorooCursor<ABTestEvent> events) throws JSONException {
JSONArray result = new JSONArray();
ABTestEvent model = new ABTestEvent();
for (events.moveToFirst(); !events.isAfterLast(); events.moveToNext()) {
model.readFromCursor(events);
JSONObject event = new JSONObject();
event.put(KEY_TEST, model.getValue(ABTestEvent.TEST_NAME));
event.put(KEY_VARIANT, model.getValue(ABTestEvent.TEST_VARIANT));
if (model.getValue(ABTestEvent.ACTIVATED_USER) > 0) {
event.put(KEY_INITIAL, true);
} else {
event.put(KEY_ACTIVATION, true);
}
result.put(event);
}
return result;
}
}

@ -1,112 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service.abtesting;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.RestClient;
/**
* Invoker for communicating with the Astrid Analytics server
* @author Sam
*
*/
public class ABTestInvoker {
/** NOTE: these values are development values and will not work on production */
private static final String URL = "http://analytics.astrid.com/api/2/";
public static final String AB_RETENTION_METHOD = "ab_retention";
public static final String AB_ACTIVATION_METHOD = "ab_activation";
private static final String ACQUISITION_METHOD = "acquisition";
private static final String API_KEY = "ryyubd";
private static final String API_SECRET = "q9ef3i";
private static final String PREF_REPORTED_ACQUISITION = "p_reported_acq";
@Autowired private RestClient restClient;
public ABTestInvoker() {
DependencyInjectionService.getInstance().inject(this);
}
public void reportAcquisition() {
}
/**
* Posts the payload to the analytics server
* @param payload - JSONArray of data points. Created by the
* helper method in ABTestReportingService
* @return
* @throws IOException
*/
public JSONObject post(String method, JSONArray payload) throws IOException {
try {
HttpEntity postData = createPostData(payload);
String response = restClient.post(URL + method, postData);
JSONObject object = new JSONObject(response);
if (object.getString("status").equals("error")) {
throw new IOException("Error reporting ABTestEvent: " +
object.optString("message"));
}
return object;
} catch (JSONException e) {
throw new IOException(e.getMessage());
}
}
/**
* Converts the JSONArray payload into an HTTPEntity suitable for
* POSTing.
* @param payload
* @return
*/
private HttpEntity createPostData(JSONArray payload) throws IOException {
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("apikey", API_KEY));
if (payload != null) {
params.add(new BasicNameValuePair("payload", payload.toString()));
}
StringBuilder sigBuilder = new StringBuilder();
for(NameValuePair entry : params) {
if(entry.getValue() == null) {
continue;
}
String key = entry.getName();
String value = entry.getValue();
sigBuilder.append(key).append(value);
}
sigBuilder.append(API_SECRET);
// String signature = DigestUtils.md5Hex(sigBuilder.toString());
// params.add(new BasicNameValuePair("sig", signature));
try {
return new UrlEncodedFormEntity(params, HTTP.UTF_8);
} catch (UnsupportedEncodingException e) {
throw new IOException("Unsupported URL encoding");
}
}
}

@ -1,143 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service.abtesting;
import java.util.HashMap;
import java.util.Set;
import android.content.Context;
import com.todoroo.astrid.utility.Constants;
/**
* Helper class to define options with their probabilities and descriptions
* @author Sam Bosley <sam@astrid.com>
*
*/
public class ABTests {
public ABTests() {
bundles = new HashMap<String, ABTestBundle>();
initialize();
}
/**
* Initialization for any tests that require a context or other logic
* to be initialized should go here. This method is called from the startup
* service before any test choices are made, so it is safe to add
* tests here. It's also ok if this method is a no-op sometimes.
* @param context
*/
public void externalInit(Context context) {
//
}
/**
* Gets the integer array of weighted probabilities for an option key
* @param key
* @return
*/
public synchronized int[] getProbsForTestKey(String key, boolean newUser) {
if (bundles.containsKey(key)) {
ABTestBundle bundle = bundles.get(key);
if (newUser) {
return bundle.newUserProbs;
} else {
return bundle.existingUserProbs;
}
} else {
return null;
}
}
/**
* Gets the string array of option descriptions for an option key
* @param key
* @return
*/
public String[] getDescriptionsForTestKey(String key) {
if (bundles.containsKey(key)) {
ABTestBundle bundle = bundles.get(key);
return bundle.descriptions;
} else {
return null;
}
}
/**
* Returns the description for a particular choice of the given option
* @param testKey
* @param optionIndex
* @return
*/
public String getDescriptionForTestOption(String testKey, int optionIndex) {
if (bundles.containsKey(testKey)) {
ABTestBundle bundle = bundles.get(testKey);
if (bundle.descriptions != null && optionIndex < bundle.descriptions.length) {
return bundle.descriptions[optionIndex];
}
}
return null;
}
public Set<String> getAllTestKeys() {
return bundles.keySet();
}
/**
* Maps keys (i.e. preference key identifiers) to feature weights and descriptions
*/
private final HashMap<String, ABTestBundle> bundles;
private static class ABTestBundle {
protected final int[] newUserProbs;
protected final int[] existingUserProbs;
protected final String[] descriptions;
protected ABTestBundle(int[] newUserProbs, int[] existingUserProbs, String[] descriptions) {
this.newUserProbs = newUserProbs;
this.existingUserProbs = existingUserProbs;
this.descriptions = descriptions;
}
}
public boolean isValidTestKey(String key) {
return bundles.containsKey(key);
}
/**
* A/B testing options are defined below according to the following spec:
*
* @param testKey = "<key>"
* --This key is used to identify the option in the application and in the preferences
*
* @param newUserProbs = { int, int, ... }
* @param existingUserProbs = { int, int, ... }
* --The different choices in an option correspond to an index in the probability array.
* Probabilities are expressed as integers to easily define relative weights. For example,
* the array { 1, 2 } would mean option 0 would happen one time for every two occurrences of option 1
*
* The first array is used for new users and the second is used for existing/upgrading users,
* allowing us to specify different distributions for each group.
*
* (optional)
* @param descriptions = { "...", "...", ... }
* --A string description of each option. Useful for tagging events. The index of
* each description should correspond to the events location in the probability array
* (i.e. the arrays should be the same length if this one exists)
*
*/
public void addTest(String testKey, int[] newUserProbs, int[] existingUserProbs, String[] descriptions, boolean appliesToAstridLite) {
if (!Constants.ASTRID_LITE || (Constants.ASTRID_LITE && appliesToAstridLite)) {
ABTestBundle bundle = new ABTestBundle(newUserProbs, existingUserProbs, descriptions);
bundles.put(testKey, bundle);
}
}
private void initialize() {
//
}
}

@ -1,100 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.ABTestEventDao;
import com.todoroo.astrid.data.ABTestEvent;
import com.todoroo.astrid.service.abtesting.ABChooser;
import com.todoroo.astrid.service.abtesting.ABTests;
import com.todoroo.astrid.test.DatabaseTestCase;
public class ABTestingServiceTest extends DatabaseTestCase {
@Autowired ABTestEventDao abTestEventDao;
@Autowired ABChooser abChooser;
@Autowired ABTests abTests;
public void testReportInitialEventNewUser() {
testInitialEvents(true, false);
}
public void testReportInitialEventExistingUser() {
testInitialEvents(false, true);
}
private void testInitialEvents(boolean newUser, boolean activatedUser) {
testInterval(newUser, activatedUser, 0);
}
public void testIntervalEventWithShortIntervalNewUser() {
testIntervalEventWithShortInterval(true, false);
}
public void testIntervalEventWithShortIntervalExistingUser() {
testIntervalEventWithShortInterval(false, true);
}
public void testIntervalEventWithLongIntervalNewUser() {
testIntervalEventWithLongInterval(true, false);
}
public void testIntervalEventWithLongIntervalExistingUser() {
testIntervalEventWithLongInterval(false, true);
}
private void testIntervalEventWithShortInterval(boolean newUser, boolean activatedUser) {
testInterval(newUser, activatedUser, 3);
}
private void testIntervalEventWithLongInterval(boolean newUser, boolean activatedUser) {
testInterval(newUser, activatedUser, 14);
}
private void testInterval(boolean newUser, boolean activatedUser, int testInterval) {
abChooser.makeChoicesForAllTests(newUser, activatedUser);
abTestEventDao.createTestEventWithTimeInterval(TEST_NAME, testInterval);
TodorooCursor<ABTestEvent> events = abTestEventDao.query(
Query.select(ABTestEvent.PROPERTIES)
.where(ABTestEvent.TEST_NAME.eq(TEST_NAME))
.orderBy(Order.asc(ABTestEvent.TIME_INTERVAL)));
try {
int maxIntervalIndex = AndroidUtilities.indexOf(ABTestEvent.TIME_INTERVALS, testInterval);
assertEquals(maxIntervalIndex + 1, events.getCount());
for (int i = 0; i < events.getCount(); i++) {
events.moveToNext();
ABTestEvent event = new ABTestEvent(events);
assertExpectedValues(event, newUser, activatedUser, ABTestEvent.TIME_INTERVALS[i]);
}
} finally {
events.close();
}
}
private void assertExpectedValues(ABTestEvent event, boolean newUser, boolean activatedUser, int timeInterval) {
assertEquals(TEST_NAME, event.getValue(ABTestEvent.TEST_NAME));
assertEquals(newUser ? 1 : 0, event.getValue(ABTestEvent.NEW_USER).intValue());
assertEquals(activatedUser ? 1 : 0, event.getValue(ABTestEvent.ACTIVATED_USER).intValue());
assertEquals(timeInterval, event.getValue(ABTestEvent.TIME_INTERVAL).intValue());
}
@Override
protected void setUp() throws Exception {
super.setUp();
abTests.addTest(TEST_NAME, new int[] { 9, 1 } , new int[] { 1, 9 }, TEST_OPTIONS, true);
Preferences.clear(TEST_NAME);
}
private static final String TEST_NAME = "test_experiment";
private static final String[] TEST_OPTIONS = new String[] { "opt-1", "opt-2" };
}
Loading…
Cancel
Save