Brion VIBBER has submitted this change and it was merged. Change subject: Add EventLogging for Edit Funnel ......................................................................
Add EventLogging for Edit Funnel - Fix EL to actually work - Send UA properly Change-Id: I6225c3df43f739b38f145669b4ae7146735a6383 --- M wikipedia/src/main/java/org/wikipedia/Utils.java M wikipedia/src/main/java/org/wikipedia/WikipediaApp.java A wikipedia/src/main/java/org/wikipedia/analytics/EditFunnel.java A wikipedia/src/main/java/org/wikipedia/analytics/EventLoggingEvent.java A wikipedia/src/main/java/org/wikipedia/analytics/Funnel.java A wikipedia/src/main/java/org/wikipedia/analytics/FunnelManager.java M wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java M wikipedia/src/main/java/org/wikipedia/editing/DoEditTask.java M wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java M wikipedia/src/main/java/org/wikipedia/editing/SuccessEditResult.java D wikipedia/src/main/java/org/wikipedia/eventlogging/EventLoggingEvent.java 11 files changed, 369 insertions(+), 103 deletions(-) Approvals: Brion VIBBER: Verified; Looks good to me, approved jenkins-bot: Verified diff --git a/wikipedia/src/main/java/org/wikipedia/Utils.java b/wikipedia/src/main/java/org/wikipedia/Utils.java index ea7ed45..56144ee 100644 --- a/wikipedia/src/main/java/org/wikipedia/Utils.java +++ b/wikipedia/src/main/java/org/wikipedia/Utils.java @@ -386,4 +386,16 @@ view.setTextDirection(Utils.isLangRTL(lang) ? View.TEXT_DIRECTION_RTL : View.TEXT_DIRECTION_LTR); } } + + /** + * Returns db name for given site + * + * WARNING: HARDCODED TO WORK FOR WIKIPEDIA ONLY + * + * @param site Site object to get dbname for + * @return dbname for given site object + */ + public static String getDBNameForSite(Site site) { + return site.getLanguage() + "wiki"; + } } diff --git a/wikipedia/src/main/java/org/wikipedia/WikipediaApp.java b/wikipedia/src/main/java/org/wikipedia/WikipediaApp.java index f233a96..c7f566a 100644 --- a/wikipedia/src/main/java/org/wikipedia/WikipediaApp.java +++ b/wikipedia/src/main/java/org/wikipedia/WikipediaApp.java @@ -12,6 +12,7 @@ import org.acra.*; import org.acra.annotation.*; import org.mediawiki.api.json.*; +import org.wikipedia.analytics.*; import org.wikipedia.data.*; import org.wikipedia.editing.*; import org.wikipedia.editing.summaries.*; @@ -279,6 +280,15 @@ return userInfoStorage; } + private FunnelManager funnelManager; + public FunnelManager getFunnelManager() { + if (funnelManager == null) { + funnelManager = new FunnelManager(this); + } + + return funnelManager; + } + private static boolean wikipediaZeroDisposition = false; public static void setWikipediaZeroDisposition(boolean b) { wikipediaZeroDisposition = b; diff --git a/wikipedia/src/main/java/org/wikipedia/analytics/EditFunnel.java b/wikipedia/src/main/java/org/wikipedia/analytics/EditFunnel.java new file mode 100644 index 0000000..6962f40 --- /dev/null +++ b/wikipedia/src/main/java/org/wikipedia/analytics/EditFunnel.java @@ -0,0 +1,120 @@ +package org.wikipedia.analytics; + +import org.json.*; +import org.wikipedia.*; + +import java.util.*; + +public class EditFunnel extends Funnel { + private static final String SCHEMA_NAME = "MobileWikiAppEdit"; + private static final int REV_ID = 8198182; + + private final String editSessionToken; + private final PageTitle title; + + public EditFunnel(WikipediaApp app, PageTitle title) { + super(app, SCHEMA_NAME, REV_ID); + editSessionToken = UUID.randomUUID().toString(); + this.title = title; + } + + @Override + protected JSONObject preprocessData(JSONObject eventData) { + try { + eventData.put("editSessionToken", editSessionToken); + if (getApp().getUserInfoStorage().isLoggedIn()) { + eventData.put("userName", getApp().getUserInfoStorage().getUser().getUsername()); + } + eventData.put("pageNS", title.getNamespace()); + } catch (JSONException e) { + // This never happens either + throw new RuntimeException(e); + } + return eventData; + } + + protected void log(Object... params) { + super.log(title.getSite(), params); + } + + public void logStart() { + log( + "action", "start" + ); + } + + public void logPreview() { + log( + "action", "preview" + ); + } + + public void logSaved(int revID) { + log( + "action", "saved", + "revID", revID + ); + + } + + public void logLoginAttempt() { + log( + "action", "loginAttempt" + ); + + } + + public void logLoginSuccess() { + log( + "action", "loginSuccess" + ); + } + + public void logLoginFailure() { + log( + "action", "loginFailure" + ); + + } + + public void logCaptchaShown() { + log( + "action", "captchaShown" + ); + + } + + public void logCaptchaFailure() { + log( + "action", "captchaFailure" + ); + } + + public void logAbuseFilterWarning(String code) { + log( + "action", "abuseFilterWarning", + "abuseFilterCode", code + ); + } + + public void logAbuseFilterError(String code) { + log( + "action", "abuseFilterError", + "abuseFilterCode", code + ); + + } + + public void logSaveAnonExplicit() { + log( + "action", "saveAnonExplicit" + ); + } + + public void logError(String code) { + log( + "action", "error", + "errorText", code + ); + } +} diff --git a/wikipedia/src/main/java/org/wikipedia/analytics/EventLoggingEvent.java b/wikipedia/src/main/java/org/wikipedia/analytics/EventLoggingEvent.java new file mode 100644 index 0000000..3b79820 --- /dev/null +++ b/wikipedia/src/main/java/org/wikipedia/analytics/EventLoggingEvent.java @@ -0,0 +1,82 @@ +package org.wikipedia.analytics; + +import android.net.*; +import android.util.*; +import com.github.kevinsawicki.http.*; +import org.json.*; +import org.wikipedia.concurrency.*; + +/** + * Base class for all various types of events that are logged to EventLogging. + * + * Each Schema has its own class, and has its own constructor that makes it easy + * to call from everywhere without having to duplicate param info at all places. + * Updating schemas / revisions is also easier this way. + */ +public class EventLoggingEvent { + private static final String EVENTLOG_URL = "https://bits.wikimedia.org/event.gif"; + + private final JSONObject data; + private final String userAgent; + + /** + * Create an EventLoggingEvent that logs to a given revision of a given schema with + * the gven data payload. + * + * @param schema Schema name (as specified on meta.wikimedia.org) + * @param revID Revision of the schema to log to + * @param wiki DBName (enwiki, dewiki, etc) of the wiki in which we are operating + * @param userAgent User-Agent string to use for this request + * @param eventData Data for the actual event payload. Considered to be + * + */ + public EventLoggingEvent(String schema, int revID, String wiki, String userAgent, JSONObject eventData) { + data = new JSONObject(); + try { + data.put("schema", schema); + data.put("revision", revID); + data.put("wiki", wiki); + data.put("event", eventData); + } catch (JSONException e) { + throw new RuntimeException(e); + } + this.userAgent = userAgent; + } + + /** + * Log the current event. + * + * Returns immediately after queueing the network request in the background. + */ + public void log() { + new LogEventTask(data).execute(); + } + + private class LogEventTask extends SaneAsyncTask<Integer> { + private final JSONObject data; + public LogEventTask(JSONObject data) { + super(SINGLE_THREAD); + this.data = data; + } + + @Override + public Integer performTask() throws Throwable { + String dataURL = Uri.parse(EVENTLOG_URL) + .buildUpon().query(data.toString()) + .build().toString(); + Log.d("Wikipedia", "hitting " + dataURL); + return HttpRequest.get(dataURL).header("User-Agent", userAgent).code(); + } + + @Override + public void onFinish(Integer result) { + Log.d("Wikipedia", "result is " + result); + } + + @Override + public void onCatch(Throwable caught) { + // Do nothing bad. EL data is ok to lose. + Log.d("Wikipedia", "Lost EL data: " + data.toString()); + } + } +} diff --git a/wikipedia/src/main/java/org/wikipedia/analytics/Funnel.java b/wikipedia/src/main/java/org/wikipedia/analytics/Funnel.java new file mode 100644 index 0000000..0e63647 --- /dev/null +++ b/wikipedia/src/main/java/org/wikipedia/analytics/Funnel.java @@ -0,0 +1,80 @@ +package org.wikipedia.analytics; + + +import android.util.*; +import org.json.*; +import org.wikipedia.*; + +public abstract class Funnel { + private final String schemaName; + private final int revision; + private final WikipediaApp app; + + protected Funnel(WikipediaApp app, String schemaName, int revision) { + this.app = app; + this.schemaName = schemaName; + this.revision = revision; + } + + protected WikipediaApp getApp() { + return app; + } + /** + * Optionally pre-process the event data before sending to EL. + * + * @param eventData Event Data so far collected + * @return Event Data to be sent to server + */ + protected JSONObject preprocessData(JSONObject eventData) { + return eventData; + } + + /** + * Logs an event. + * + * @param site The wiki in which this action was performed. + * @param params Actual data for the event. Considered to be an array + * of alternating key and value items (for easier + * use in subclass constructors). + * + * For example, what would be expressed in a more sane + * language as: + * + * .log({ + * "page": "List of mass murderers", + * "section": "2014" + * }); + * + * would be expressed here as + * + * .log( + * "page", "List of mass murderers", + * "section", "2014" + * ); + * + * This format should be only used in subclass methods directly. + * The subclass methods should take more explicit parameters + * depending on what they are logging. + */ + protected void log(Site site, Object... params) { + JSONObject eventData = new JSONObject(); + + try { + for (int i = 0; i < params.length; i += 2) { + eventData.put(params[i].toString(), params[i + 1]); + Log.d("Wikipedia", params[i] + " " + params[i + 1]); + } + } catch (JSONException e) { + // This does not happen + throw new RuntimeException(e); + } + + new EventLoggingEvent( + schemaName, + revision, + Utils.getDBNameForSite(site), + app.getUserAgent(), + preprocessData(eventData) + ).log(); + } +} diff --git a/wikipedia/src/main/java/org/wikipedia/analytics/FunnelManager.java b/wikipedia/src/main/java/org/wikipedia/analytics/FunnelManager.java new file mode 100644 index 0000000..3dc0b09 --- /dev/null +++ b/wikipedia/src/main/java/org/wikipedia/analytics/FunnelManager.java @@ -0,0 +1,25 @@ +package org.wikipedia.analytics; + +import org.wikipedia.*; + +import java.util.*; + +/** + * Creates and stores analytics tracking funnels. + */ +public class FunnelManager { + private final WikipediaApp app; + private final Hashtable<PageTitle, EditFunnel> editFunnels = new Hashtable<PageTitle, EditFunnel>(); + + public FunnelManager(WikipediaApp app) { + this.app = app; + } + + public EditFunnel getEditFunnel(PageTitle title) { + if (!editFunnels.containsKey(title)) { + editFunnels.put(title, new EditFunnel(app, title)); + } + + return editFunnels.get(title); + } +} diff --git a/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java b/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java index 2da61b8..ae23bfd 100644 --- a/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java @@ -3,13 +3,13 @@ import android.app.*; import android.net.*; import android.os.*; -import android.support.v7.app.ActionBarActivity; -import android.util.*; +import android.support.v7.app.*; import android.view.*; import android.widget.*; import com.squareup.picasso.*; import org.mediawiki.api.json.*; import org.wikipedia.*; +import org.wikipedia.Utils; public class CaptchaHandler { private final Activity activity; @@ -68,6 +68,10 @@ outState.putParcelable("captcha", captchaResult); } + public boolean isActive() { + return captchaResult != null; + } + public void handleCaptcha(CaptchaResult captchaResult) { this.captchaResult = captchaResult; handleCaptcha(false); diff --git a/wikipedia/src/main/java/org/wikipedia/editing/DoEditTask.java b/wikipedia/src/main/java/org/wikipedia/editing/DoEditTask.java index 7ee6672..c5d8070 100644 --- a/wikipedia/src/main/java/org/wikipedia/editing/DoEditTask.java +++ b/wikipedia/src/main/java/org/wikipedia/editing/DoEditTask.java @@ -57,7 +57,7 @@ JSONObject edit = resultJSON.optJSONObject("edit"); String status = edit.optString("result"); if (status.equals("Success")) { - return new SuccessEditResult(); + return new SuccessEditResult(edit.optInt("newrevid")); } else if (status.equals("Failure")) { if (edit.has("captcha")) { return new CaptchaResult( diff --git a/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java b/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java index 449caa0..59b5c8b 100644 --- a/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java +++ b/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java @@ -13,6 +13,7 @@ import org.mediawiki.api.json.*; import org.wikipedia.*; import org.wikipedia.Utils; +import org.wikipedia.analytics.*; import org.wikipedia.editing.summaries.*; import org.wikipedia.login.*; import org.wikipedia.page.*; @@ -52,6 +53,8 @@ private View editSaveOptionsContainer; private View editSaveOptionAnon; private View editSaveOptionLogIn; + + private EditFunnel funnel; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -125,6 +128,7 @@ public void onClick(View view) { wasSaveOptionsUsed = true; Utils.fadeOut(editSaveOptionsContainer); + funnel.logSaveAnonExplicit(); doSave(); } }); @@ -134,6 +138,7 @@ public void onClick(View view) { wasSaveOptionsUsed = true; Intent loginIntent = new Intent(EditSectionActivity.this, LoginActivity.class); + funnel.logLoginAttempt(); startActivityForResult(loginIntent, LoginActivity.REQUEST_LOGIN); } }); @@ -141,6 +146,10 @@ Utils.setTextDirection(sectionText, title.getSite().getLanguage()); fetchSectionText(); + + funnel = app.getFunnelManager().getEditFunnel(title); + + funnel.logStart(); } @Override @@ -149,6 +158,9 @@ if (resultCode == LoginActivity.RESULT_LOGIN_SUCCESS) { Utils.fadeOut(editSaveOptionsContainer); doSave(); + funnel.logLoginSuccess(); + } else { + funnel.logLoginFailure(); } } } @@ -162,7 +174,6 @@ app.getEditTokenStorage().get(title.getSite(), new EditTokenStorage.TokenRetreivedCallback() { @Override public void onTokenRetreived(final String token) { - new DoEditTask(EditSectionActivity.this, title, sectionText.getText().toString(), section.getId(), token, editSummaryHandler.getSummary(section.getHeading())) { @Override public void onBeforeExecute() { @@ -226,17 +237,24 @@ @Override public void onFinish(EditingResult result) { if (result instanceof SuccessEditResult) { + funnel.logSaved(((SuccessEditResult) result).getRevID()); progressDialog.dismiss(); setResult(EditHandler.RESULT_REFRESH_PAGE); Toast.makeText(EditSectionActivity.this, R.string.edit_saved_successfully, Toast.LENGTH_LONG).show(); Utils.hideSoftKeyboard(EditSectionActivity.this); finish(); } else if (result instanceof CaptchaResult) { + if (captchaHandler.isActive()) { + // Captcha entry failed! + funnel.logCaptchaFailure(); + } captchaHandler.handleCaptcha((CaptchaResult) result); + funnel.logCaptchaShown(); } else if (result instanceof AbuseFilterEditResult) { abusefilterEditResult = (AbuseFilterEditResult) result; handleAbuseFilter(); } else { + funnel.logError(result.getResult()); // Expand to do everything. onCatch(null); } @@ -250,6 +268,11 @@ private void handleAbuseFilter() { if (abusefilterEditResult == null) { return; + } + if (abusefilterEditResult.getType() == AbuseFilterEditResult.TYPE_ERROR) { + funnel.logAbuseFilterError(abusefilterEditResult.getCode()); + } else { + funnel.logAbuseFilterWarning(abusefilterEditResult.getCode()); } JSONObject payload = new JSONObject(); try { @@ -304,6 +327,7 @@ } else { Utils.hideSoftKeyboard(this); editPreviewFragment.showPreview(title, sectionText.getText().toString()); + funnel.logPreview(); } return true; default: diff --git a/wikipedia/src/main/java/org/wikipedia/editing/SuccessEditResult.java b/wikipedia/src/main/java/org/wikipedia/editing/SuccessEditResult.java index e444030..6420e21 100644 --- a/wikipedia/src/main/java/org/wikipedia/editing/SuccessEditResult.java +++ b/wikipedia/src/main/java/org/wikipedia/editing/SuccessEditResult.java @@ -3,12 +3,19 @@ import android.os.*; public class SuccessEditResult extends EditingResult { - public SuccessEditResult() { + private final int revID; + public SuccessEditResult(int revID) { super("Success"); + this.revID = revID; } private SuccessEditResult(Parcel in) { super(in); + revID = in.readInt(); + } + + public int getRevID() { + return revID; } public static final Parcelable.Creator<SuccessEditResult> CREATOR diff --git a/wikipedia/src/main/java/org/wikipedia/eventlogging/EventLoggingEvent.java b/wikipedia/src/main/java/org/wikipedia/eventlogging/EventLoggingEvent.java deleted file mode 100644 index 3709289..0000000 --- a/wikipedia/src/main/java/org/wikipedia/eventlogging/EventLoggingEvent.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.wikipedia.eventlogging; - -import android.net.*; -import android.util.*; -import com.github.kevinsawicki.http.*; -import org.json.*; -import org.wikipedia.concurrency.*; - -/** - * Base class for all various types of events that are logged to EventLogging. - * - * Each Schema has its own class, and has its own constructor that makes it easy - * to call from everywhere without having to duplicate param info at all places. - * Updating schemas / revisions is also easier this way. - */ -public abstract class EventLoggingEvent { - private static final String EVENTLOG_URL = "https://bits.wikimedia.org/event.gif"; - - private final JSONObject data; - - /** - * Create an EventLoggingEvent that logs to a given revision of a given schema with - * the gven data payload. - * - * @param schema Schema name (as specified on meta.wikimedia.org) - * @param revID Revision of the schema to log to - * @param payload Data for the actual event payload. Considered to be - * an array of alternating key and value items (for easier - * construction in subclass constructors). - * - * For example, what would be expressed in a more sane - * language as: - * - * new SomeSubClass("Schema", 4200, { - * "page": "List of mass murderers", - * "section": "2014" - * }); - * - * would be expressed here as - * - * new SomeSubClass("Schema", 4200, - * "page", "List of mass murderers", - * "section", "2014" - * ); - * - * This format should be only used in subclass constructors. - * The subclass constructors should take more explicit parameters - * depending on what they are logging. - */ - protected EventLoggingEvent(String schema, int revID, String... payload) { - data = new JSONObject(); - try { - data.put("schema", schema); - data.put("revision", revID); - - JSONObject event = new JSONObject(); - - for (int i = 0; i < payload.length; i += 2) { - event.put(payload[i], payload[i + 1]); - } - - data.put("event", event); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - /** - * Log the current event. - * - * Returns immediately after queueing the network request in the background. - */ - public void log() { - new LogEventTask(data).execute(); - } - - private static class LogEventTask extends SaneAsyncTask<Boolean> { - private final JSONObject data; - public LogEventTask(JSONObject data) { - super(SINGLE_THREAD); - this.data = data; - } - - @Override - public Boolean performTask() throws Throwable { - String dataURL = Uri.parse(EVENTLOG_URL) - .buildUpon().query(data.toString()) - .build().toString(); - return HttpRequest.get(dataURL).ok(); - } - - @Override - public void onCatch(Throwable caught) { - // Do nothing bad. EL data is ok to lose. - Log.d("Wikipedia", "Lost EL data: " + data.toString()); - } - } -} -- To view, visit https://gerrit.wikimedia.org/r/126849 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I6225c3df43f739b38f145669b4ae7146735a6383 Gerrit-PatchSet: 9 Gerrit-Project: apps/android/wikipedia Gerrit-Branch: master Gerrit-Owner: Yuvipanda <yuvipa...@gmail.com> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits