Bug 1633568 - Introduce a installation ping. r=Grisha, a=RyanVM FIREFOX_ESR_68_8_X_RELBRANCH
authorPetru-Mugurel Lingurar <petru.lingurar@softvision.ro>
Thu, 14 May 2020 05:57:13 +0000
branchFIREFOX_ESR_68_8_X_RELBRANCH
changeset 524814 779dee1241adec18384fa1ff2c7d8ed297cc8b85
parent 524813 9c08bb26d3af807efc3f1d80c067a0d3c7a2f57b
child 524815 fc5a91e400243cc2c2c6a6ecc0d211be26a9662b
push id1030
push userryanvm@gmail.com
push dateThu, 14 May 2020 19:53:46 +0000
treeherdermozilla-esr68@fc5a91e40024 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGrisha, RyanVM
bugs1633568
milestone68.8.1
Bug 1633568 - Introduce a installation ping. r=Grisha, a=RyanVM This mobile-specific ping is intended to keep track of installs and Adjust attribution. The app should send two installation pings, based on different reasons: One sent immediately after the app starts. One sent immediately after the Adjust attribution data becomes available. If the app is later deleted and installed again, the installation pings must be sent again. This will will be a modern ping, sent to hostname/submit/mobile/docType/appVersion/docId. Differential Revision: https://phabricator.services.mozilla.com/D74073
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryInstallationPingDelegate.java
mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryInstallationPingBuilder.java
mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryInstallationPingStore.java
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -136,16 +136,17 @@ import org.mozilla.gecko.sync.repositori
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
+import org.mozilla.gecko.telemetry.TelemetryInstallationPingDelegate;
 import org.mozilla.gecko.telemetry.TelemetryUploadService;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.telemetry.TelemetryActivationPingDelegate;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.CommitEventSource;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.PwaConfirm;
@@ -319,25 +320,27 @@ public class BrowserApp extends GeckoApp
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();
     private final TelemetryActivationPingDelegate mTelemetryActivationPingDelegate = new TelemetryActivationPingDelegate();
+    private final TelemetryInstallationPingDelegate mTelemetryInstallationPingDelegate = new TelemetryInstallationPingDelegate();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             new ScreenshotDelegate(),
             new BookmarkStateChangeDelegate(),
             new ReaderViewBookmarkPromotion(),
             mTelemetryCorePingDelegate,
             mTelemetryActivationPingDelegate,
+            mTelemetryInstallationPingDelegate,
             new OfflineTabStatusDelegate(),
-            new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
+            new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate, mTelemetryInstallationPingDelegate)
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
     private OnboardingHelper mOnboardingHelper;       // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryInstallationPingDelegate.java
@@ -0,0 +1,128 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.adjust.sdk.AdjustAttribution;
+
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.adjust.AttributionHelperListener;
+import org.mozilla.gecko.delegates.BrowserAppDelegate;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetryInstallationPingBuilder;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetryInstallationPingBuilder.PingReason;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
+import org.mozilla.gecko.telemetry.stores.TelemetryInstallationPingStore;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.IOException;
+
+public class TelemetryInstallationPingDelegate
+        extends BrowserAppDelegate
+        implements AttributionHelperListener {
+
+    private static final String LOGTAG = "InstallPingDelegate";
+
+    @Override
+    public void onStart(BrowserApp browserApp) {
+        if (!TelemetryUploadService.isUploadEnabledByAppConfig(browserApp)) {
+            return;
+        }
+
+        // Keep everything off of main thread. Don't need to burden it with telemetry.
+        ThreadUtils.postToBackgroundThread(() -> {
+            TelemetryInstallationPingStore store;
+            try {
+                store = new TelemetryInstallationPingStore();
+            } catch (IllegalStateException e) {
+                // The store constructor might throw an IllegalStateException if it cannot access
+                // the store directory.
+                // This has been observed on CI mochitests, not sure about if this would also reproduce
+                // in the real world.
+                // We'll retry at the next app start.
+                Log.w(LOGTAG, "Cannot access ping's storage directory. Will retry later");
+                return;
+            }
+
+            // First allow for stored pings to be re-uploaded if the previous upload did not succeed.
+            // (A successful upload would delete the pings persisted to disk)
+            if (store.getCount() != 0) {
+                store.queuePingsForUpload(new TelemetryUploadAllPingsImmediatelyScheduler());
+            }
+
+            // Only need one of each pings. Check if we should create a new one.
+            if (!TelemetryInstallationPingStore.hasLightPingBeenQueuedForUpload()) {
+                TelemetryOutgoingPing ping = new TelemetryInstallationPingBuilder()
+                        .setReason(PingReason.APP_STARTED)
+                        .build();
+
+                try {
+                    store.storePing(ping);
+                    store.queuePingsForUpload(new TelemetryUploadAllPingsImmediatelyScheduler());
+                    store.setLightPingQueuedForUpload();
+                } catch (IOException e) {
+                    // #storePing() might throw in the process of persisting to disk.
+                    // Nothing to do. At the next app start we'll try again to create a new ping,
+                    // store and upload that.
+                    Log.w(LOGTAG, "Could not store ping. Will try again later");
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onAttributionChanged(@NonNull final AdjustAttribution attribution) {
+        if (!TelemetryUploadService.isUploadEnabledByAppConfig(GeckoAppShell.getApplicationContext())) {
+            return;
+        }
+
+        // Keep everything off of main thread. Don't need to burden it with telemetry.
+        ThreadUtils.postToBackgroundThread(() -> {
+            TelemetryInstallationPingStore store;
+            try {
+                store = new TelemetryInstallationPingStore();
+            } catch (IllegalStateException e) {
+                // The store constructor might throw an IllegalStateException if it cannot access
+                // the store directory.
+                // This has been observed on CI mochitests, not sure about if this would also reproduce
+                // in the real world.
+                // Since the attributionChanged callback only fire once IRL this would mean we won't
+                // be sending the "adjust-available" ping.
+                Log.w(LOGTAG, "Cannot access ping's storage directory. " +
+                        "Cannot send the \"adjust-available\" ping");
+                return;
+            }
+
+            // First allow for stored pings to be re-uploaded if the previous upload did not succeed.
+            // (A successful upload would delete the pings persisted to disk)
+            if (store.getCount() != 0) {
+                store.queuePingsForUpload(new TelemetryUploadAllPingsImmediatelyScheduler());
+            }
+
+            // It may be possible that in the app's lifetime Adjust campaigns are changed.
+            // Sanity check that the "adjust-available" ping has not yet been send.
+            if (!TelemetryInstallationPingStore.hasFullPingBeenQueuedForUpload()) {
+                TelemetryOutgoingPing ping = new TelemetryInstallationPingBuilder()
+                        .setReason(PingReason.ADJUST_AVAILABLE)
+                        .setAdjustProperties(attribution)
+                        .build();
+
+                try {
+                    store.storePing(ping);
+                    store.queuePingsForUpload(new TelemetryUploadAllPingsImmediatelyScheduler());
+                    store.setFullPingQueuedForUpload();
+                } catch (IOException e) {
+                    // #storePing() might throw in the process of persisting to disk.
+                    // Nothing we can do. The "adjust-available" ping is lost.
+                    Log.w(LOGTAG, "Could not store the \"adjust-available\" ping");
+                }
+            }
+        });
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryInstallationPingBuilder.java
@@ -0,0 +1,186 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.adjust.sdk.AdjustAttribution;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.util.DateUtil;
+import org.mozilla.gecko.util.HardwareUtils;
+
+import java.lang.reflect.Method;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class TelemetryInstallationPingBuilder extends TelemetryPingBuilder {
+    private static final String LOGTAG = "InstallPingBuilder";
+
+    public enum PingReason {
+        APP_STARTED("app-started"),
+        ADJUST_AVAILABLE("adjust-available");
+
+        PingReason(String reason) {
+            this.value = reason;
+        }
+
+        public final String value;
+    }
+
+    private static final String PING_TYPE = "installation";
+    private static final int PING_FORMAT_VERSION = 1;
+
+    private static final String PREF_KEY_SEQ_NUMBER = "installationPingSeqNumber";
+
+    private static final String PING_VERSION = "v";
+    private static final String REASON = "reason";
+    private static final String PING_QUEUED_TIMES = "seq";
+    private static final String CLIENT_ID = "client_id";
+    private static final String DEVICE_ID = "device_id";
+    private static final String LOCALE = "locale";
+    private static final String OS_NAME = "os";
+    private static final String OS_VERSION = "osversion";
+    private static final String DEVICE_MANUFACTURER = "manufacturer";
+    private static final String DEVICE_MODEL = "model";
+    private static final String DEVICE_ABI = "arch";
+    private static final String PROFILE_DATE = "profile_date";
+    private static final String PING_CREATION_TIME = "created";
+    private static final String TIMEZONE_OFFSET = "tz";
+    private static final String APP_NAME = "app_name";
+    private static final String RELEASE_CHANNEL = "channel";
+    private static final String ADJUST_CAMPAIGN = "campaign";
+    private static final String ADJUST_ADGROUP = "adgroup";
+    private static final String ADJUST_CREATIVE = "creative";
+    private static final String ADJUST_NETWORK = "network";
+
+    public TelemetryInstallationPingBuilder() {
+        super(UNIFIED_TELEMETRY_VERSION, false);
+        setPayloadConstants();
+    }
+
+    @Override
+    public String getDocType() {
+        return PING_TYPE;
+    }
+
+    @Override
+    public String[] getMandatoryFields() {
+        return new String[]{
+                PING_VERSION,
+                REASON,
+                PING_QUEUED_TIMES,
+                CLIENT_ID,
+                DEVICE_ID,
+                LOCALE,
+                OS_NAME,
+                OS_VERSION,
+                DEVICE_MANUFACTURER,
+                DEVICE_MODEL,
+                DEVICE_ABI,
+                PROFILE_DATE,
+                PING_CREATION_TIME,
+                TIMEZONE_OFFSET,
+                APP_NAME,
+                RELEASE_CHANNEL,
+        };
+    }
+
+    public @NonNull TelemetryInstallationPingBuilder setReason(@NonNull PingReason reason) {
+        payload.put(REASON, reason.value);
+
+        return this;
+    }
+
+    public @NonNull TelemetryInstallationPingBuilder setAdjustProperties(@NonNull final AdjustAttribution attribution) {
+        payload.put(ADJUST_CAMPAIGN, attribution.campaign);
+        payload.put(ADJUST_ADGROUP, attribution.adgroup);
+        payload.put(ADJUST_CREATIVE, attribution.creative);
+        payload.put(ADJUST_NETWORK, attribution.network);
+
+        return this;
+    }
+
+    private void setPayloadConstants() {
+        payload.put(PING_VERSION, PING_FORMAT_VERSION);
+        payload.put(PING_QUEUED_TIMES, incrementAndGetQueueTimes());
+        payload.put(CLIENT_ID, getGeckoClientID());
+        payload.put(DEVICE_ID, getAdvertisingId());
+        payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
+        payload.put(OS_NAME, TelemetryPingBuilder.OS_NAME);
+        payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT));
+        payload.put(DEVICE_MANUFACTURER, Build.MANUFACTURER);
+        payload.put(DEVICE_MODEL, Build.MODEL);
+        payload.put(DEVICE_ABI, HardwareUtils.getRealAbi());
+        payload.put(PROFILE_DATE, getGeckoProfileCreationDate());
+        payload.put(PING_CREATION_TIME, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()));
+        payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(Calendar.getInstance()));
+        payload.put(APP_NAME, AppConstants.MOZ_APP_BASENAME);
+        payload.put(RELEASE_CHANNEL, AppConstants.MOZ_UPDATE_CHANNEL);
+    }
+
+    private @Nullable String getGeckoClientID() {
+        // zero-ed Gecko profile that respects the expected format "8-4-4-4-12" chars
+        String clientID = "00000000-0000-0000-0000-000000000000";
+        try {
+            clientID = GeckoThread.getActiveProfile().getClientId();
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Could not get Gecko Client ID", e);
+        }
+
+        return clientID;
+    }
+
+    private @Nullable String getAdvertisingId() {
+        String advertisingId = null;
+        try {
+            final Class<?> clazz = Class.forName("org.mozilla.gecko.advertising.AdvertisingUtil");
+            final Method getAdvertisingId = clazz.getMethod("getAdvertisingId", Context.class);
+            advertisingId = (String) getAdvertisingId.invoke(clazz, GeckoAppShell.getApplicationContext());
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Could not get advertising ID", e);
+        }
+
+        return advertisingId;
+    }
+
+    // Ensure sequential increment and return in all instances.
+    private static synchronized int incrementAndGetQueueTimes() {
+        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(GeckoAppShell.getApplicationContext());
+
+        // 1-based, always incremented
+        final int incrementedSeqNumber = sharedPrefs.getInt(PREF_KEY_SEQ_NUMBER, 0) + 1;
+        sharedPrefs.edit().putInt(PREF_KEY_SEQ_NUMBER, incrementedSeqNumber).apply();
+
+        return incrementedSeqNumber;
+    }
+
+    private int getGeckoProfileCreationDate() {
+        // The method returns days since epoch. An int is enough.
+        int date = 0;
+        try {
+            date = TelemetryActivationPingBuilder.getProfileCreationDate(
+                    GeckoAppShell.getApplicationContext(),
+                    GeckoThread.getActiveProfile()).intValue();
+        } catch (NullPointerException e) {
+            Log.w(LOGTAG, "Could not get Gecko profile creation date", e);
+        }
+
+        return date;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryInstallationPingStore.java
@@ -0,0 +1,102 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.stores;
+
+import android.content.SharedPreferences;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
+
+import java.io.File;
+
+public class TelemetryInstallationPingStore extends TelemetryJSONFilePingStore {
+    private static final String PREF_KEY_WAS_LIGHT_PING_STORED = "wasLightInstallationPingStored";
+    private static final String PREF_KEY_WAS_FULL_PING_STORED = "wasFullInstallationPingStored";
+    private static final String INSTALLATION_PING_STORE_DIR = "installation_ping";
+    private static final String DEFAULT_PROFILE = "default";
+
+    public TelemetryInstallationPingStore() {
+        super(getInstallationPingStoreDir(), getCurrentProfileName());
+    }
+
+    public TelemetryInstallationPingStore(@NonNull final File storeDir, @NonNull final String profileName) {
+        super(storeDir, profileName);
+    }
+
+    @Override
+    public void maybePrunePings() {
+        // no-op
+        // Successfully uploaded pings will be deleted in onUploadAttemptComplete(..).
+    }
+
+    public void queuePingsForUpload(@NonNull final TelemetryUploadAllPingsImmediatelyScheduler scheduler) {
+        scheduler.scheduleUpload(GeckoAppShell.getApplicationContext(), this);
+    }
+
+    public static boolean hasLightPingBeenQueuedForUpload() {
+        return getSharedPrefs().getBoolean(PREF_KEY_WAS_LIGHT_PING_STORED, false);
+    }
+
+    public static boolean hasFullPingBeenQueuedForUpload() {
+        return getSharedPrefs().getBoolean(PREF_KEY_WAS_FULL_PING_STORED, false);
+    }
+
+    public void setLightPingQueuedForUpload() {
+        getSharedPrefs().edit().putBoolean(PREF_KEY_WAS_LIGHT_PING_STORED, true).apply();
+    }
+
+    public void setFullPingQueuedForUpload() {
+        getSharedPrefs().edit().putBoolean(PREF_KEY_WAS_FULL_PING_STORED, true).apply();
+    }
+
+    private static @NonNull SharedPreferences getSharedPrefs() {
+        return GeckoSharedPrefs.forProfile(GeckoAppShell.getApplicationContext());
+    }
+
+    private static @NonNull File getInstallationPingStoreDir() {
+        return GeckoAppShell.getApplicationContext().getFileStreamPath(INSTALLATION_PING_STORE_DIR);
+    }
+
+    private static @NonNull String getCurrentProfileName() {
+        return GeckoThread.getActiveProfile() != null ?
+                GeckoThread.getActiveProfile().getName() :
+                DEFAULT_PROFILE;
+    }
+
+
+    // Class needs to be Parcelable as it will be passed through Intents
+    public static final Parcelable.Creator<TelemetryInstallationPingStore> CREATOR =
+            new Parcelable.Creator<TelemetryInstallationPingStore>() {
+
+        @Override
+        public TelemetryInstallationPingStore createFromParcel(final Parcel source) {
+            final String storeDirPath = source.readString();
+            final String profileName = source.readString();
+            return new TelemetryInstallationPingStore(new File(storeDirPath), profileName);
+        }
+
+        @Override
+        public TelemetryInstallationPingStore[] newArray(final int size) {
+            return new TelemetryInstallationPingStore[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(final Parcel dest, final int flags) {
+        super.writeToParcel(dest, flags);
+    }
+}