package org.wikipedia.analytics; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.gson.annotations.SerializedName; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.WikipediaApp; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.util.ReleaseUtil; import org.wikipedia.util.log.L; import java.util.UUID; /** Schemas for this abstract funnel are expected to have appInstallID and sessionToken fields. When * these fields are not present or differently named, preprocess* or get*Field should be overridden. */ /*package*/ abstract class Funnel { protected static final int SAMPLE_LOG_1K = 1000; protected static final int SAMPLE_LOG_100 = 100; protected static final int SAMPLE_LOG_10 = 10; protected static final int SAMPLE_LOG_ALL = 1; protected static final int SAMPLE_LOG_DISABLE = 0; private static final String DEFAULT_APP_INSTALL_ID_KEY = "appInstallID"; private static final String DEFAULT_SESSION_TOKEN_KEY = "sessionToken"; private final String schemaName; private final int revision; private final int sampleRate; private final String sampleRateRemoteParamName; private final WikipediaApp app; // todo: remove @SerializedName if not pickled @SerializedName("site") @Nullable private final WikiSite wiki; private final String sessionToken = UUID.randomUUID().toString(); /*package*/ Funnel(WikipediaApp app, String schemaName, int revision) { this(app, schemaName, revision, SAMPLE_LOG_ALL); } /*package*/ Funnel(WikipediaApp app, String schemaName, int revision, @Nullable WikiSite wiki) { this(app, schemaName, revision, SAMPLE_LOG_ALL, wiki); } /*package*/ Funnel(WikipediaApp app, String schemaName, int revision, int sampleRate) { this(app, schemaName, revision, sampleRate, null); } /*package*/ Funnel(WikipediaApp app, String schemaName, int revision, int sampleRate, @Nullable WikiSite wiki) { this.app = app; this.schemaName = schemaName; this.revision = revision; this.sampleRate = sampleRate; this.wiki = wiki; sampleRateRemoteParamName = schemaName + "_rate"; } 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(@NonNull JSONObject eventData) { preprocessAppInstallID(eventData); preprocessSessionToken(eventData); return eventData; } /** Invokes {@link JSONObject#put} on <code>data</code> and throws a {@link RuntimeException} on * failure. */ protected <T> void preprocessData(@NonNull JSONObject eventData, String key, T val) { try { eventData.put(key, val); } catch (JSONException e) { throw new RuntimeException("key=" + key + " val=" + val, e); } } /** Invoked by {@link #preprocessData(JSONObject)}. */ protected void preprocessAppInstallID(@NonNull JSONObject eventData) { preprocessData(eventData, getAppInstallIDField(), getAppInstallID()); } /** Invoked by {@link #preprocessData(JSONObject)}. */ protected void preprocessSessionToken(@NonNull JSONObject eventData) { preprocessData(eventData, getSessionTokenField(), getSessionToken()); } protected void log(Object... params) { log(wiki, params); } /** * Logs an event. * * @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(@Nullable WikiSite wiki, Object... params) { if (!app.isEventLoggingEnabled()) { // Do not send events if the user opted out of EventLogging return; } int rate = getSampleRate(); if (rate != SAMPLE_LOG_DISABLE) { boolean chosen = app.getEventLogSamplingID() % rate == 0 || ReleaseUtil.isDevRelease(); if (chosen) { JSONObject eventData = new JSONObject(); //Build the string which is logged to debug EventLogging code String logString = this.getClass().getSimpleName() + ": Sending event"; for (int i = 0; i < params.length; i += 2) { preprocessData(eventData, params[i].toString(), params[i + 1]); logString += ", event_" + params[i] + " = " + params[i + 1]; } L.d(logString); new EventLoggingEvent( schemaName, revision, wiki == null ? app.getWikiSite().dbName() : wiki.dbName(), preprocessData(eventData) ).log(); } } } /** * @return Sampling rate for this funnel, as given by the remote config parameter for this * funnel (the name of which is defined as "[schema name]_rate"), with a fallback to the * hard-coded sampling rate passed into the constructor. */ protected int getSampleRate() { return app.getRemoteConfig().getConfig().optInt(sampleRateRemoteParamName, sampleRate); } /** @return The application installation identifier field used by {@link #preprocessAppInstallID}. */ @NonNull protected String getAppInstallIDField() { return DEFAULT_APP_INSTALL_ID_KEY; } /** @return The session identifier field used by {@link #preprocessSessionToken}. */ @NonNull protected String getSessionTokenField() { return DEFAULT_SESSION_TOKEN_KEY; } /** @return The application installation identifier used by {@link #preprocessAppInstallID}. */ @Nullable protected String getAppInstallID() { return getApp().getAppInstallID(); } /** @return The session identifier used by {@link #preprocessSessionToken}. */ @Nullable protected String getSessionToken() { return sessionToken; } }