Bug 941685 - Ping metrics server with snippets impression data. r=bnicholson
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Mon, 02 Dec 2013 16:54:21 -0800
changeset 174077 ef0cf280dd12044109b59cde9bdeaa204a5b7a73
parent 174076 208332369a4a3f4d56dda6b33ad51eaa79048adf
child 174078 3fc2445c172691e544e0e563c9d18ef73d7d1dc7
push id445
push userffxbld
push dateMon, 10 Mar 2014 22:05:19 +0000
treeherdermozilla-release@dc38b741b04e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbnicholson
bugs941685
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 941685 - Ping metrics server with snippets impression data. r=bnicholson
mobile/android/app/mobile.js
mobile/android/components/Snippets.js
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -795,10 +795,13 @@ pref("snav.enabled", true);
 pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 // How frequently we check for new snippets, in seconds (1 day)
 pref("browser.snippets.updateInterval", 86400);
 
 // URL used to check for user's country code
 pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
 
+// URL used to ping metrics with stats about which snippets have been shown
+pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
+
 // This pref requires a restart to take effect.
 pref("browser.snippets.enabled", false);
--- a/mobile/android/components/Snippets.js
+++ b/mobile/android/components/Snippets.js
@@ -4,25 +4,29 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
 XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
 
 const SNIPPETS_ENABLED = Services.prefs.getBoolPref("browser.snippets.enabled");
 
 // URL to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
 
+// URL to send stats data to metrics.
+const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
+
 // URL to fetch country code, a value that's cached and refreshed once per month.
 const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
 
 // Timestamp when we last updated the user's country code.
 const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
 
 // Pref where we'll cache the user's country.
 const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
@@ -33,16 +37,30 @@ const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 
 // Should be bumped up if the snippets content format changes.
 const SNIPPETS_VERSION = 1;
 
 XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
   let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
   return Services.urlFormatter.formatURL(updateURL);
 });
 
+// Where we cache snippets data
+XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
+  return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
+});
+
+// Where we store stats about which snippets have been shown
+XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
+});
+
 XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
   return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
 });
 
 XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
   try {
     return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
   } catch (e) {
@@ -104,28 +122,26 @@ function updateSnippets() {
 }
 
 /**
  * Caches snippets server response text to `snippets.json` in profile directory.
  *
  * @param response responseText returned from snippets server
  */
 function cacheSnippets(response) {
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
   let data = gEncoder.encode(response);
-  let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
+  let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
   promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
 }
 
 /**
  * Loads snippets from cached `snippets.json`.
  */
 function loadSnippetsFromCache() {
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
-  let promise = OS.File.read(path);
+  let promise = OS.File.read(gSnippetsPath);
   promise.then(array => updateBanner(gDecoder.decode(array)), e => {
     // If snippets.json doesn't exist, update data from the server.
     if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
       update();
     } else {
       Cu.reportError("Error loading snippets from cache: " + e);
     }
   });
@@ -162,25 +178,98 @@ function updateBanner(response) {
     }
     let id = Home.banner.add({
       text: message.text,
       icon: message.icon,
       onclick: function() {
         gChromeWin.BrowserApp.addTab(message.url);
       },
       onshown: function() {
-        // XXX: 10% of the time, let the metrics server know which message was shown (bug 937373)
+        // 10% of the time, record the snippet id and a timestamp
+        if (Math.random() < .1) {
+          writeStat(message.id, new Date().toISOString());
+        }
       }
     });
     // Keep track of the message we added so that we can remove it later.
     gMessageIds.push(id);
   });
 }
 
 /**
+ * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
+ *
+ * @param snippetId unique id for snippet, sent from snippets server
+ * @param timestamp in ISO8601
+ */
+function writeStat(snippetId, timestamp) {
+  let data = gEncoder.encode(snippetId + "," + timestamp + ";");
+
+  Task.spawn(function() {
+    try {
+      let file = yield OS.File.open(gStatsPath, { append: true, write: true });
+      try {
+        yield file.write(data);
+      } finally {
+        yield file.close();
+      }
+    } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+      // If the file doesn't exist yet, create it.
+      yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
+    }
+  }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
+}
+
+/**
+ * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
+ */
+function sendStats() {
+  let promise = OS.File.read(gStatsPath);
+  promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
+    if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+      // If the file doesn't exist, there aren't any stats to send.
+    } else {
+      Cu.reportError("Error eading snippets stats: " + e);
+    }
+  });
+}
+
+/**
+ * Sends stats to metrics about which snippets have been shown.
+ * Appends snippet ids and timestamps as parameters to a GET request.
+ * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
+ *
+ * @param data contents of stats data file
+ */
+function sendStatsRequest(data) {
+  let params = [];
+  let stats = data.split(";");
+
+  // The last item in the array will be an empty string, so stop before then.
+  for (let i = 0; i < stats.length - 1; i++) {
+    let stat = stats[i].split(",");
+    params.push("s" + i + "=" + encodeURIComponent(stat[0]));
+    params.push("t" + i + "=" + encodeURIComponent(stat[1]));
+  }
+
+  let url = gStatsURL + "?" + params.join("&");
+
+  // Remove the file after succesfully sending the data.
+  _httpGetRequest(url, removeStats);
+}
+
+/**
+ * Removes text file where we store snippets stats.
+ */
+function removeStats() {
+  let promise = OS.File.remove(gStatsPath);
+  promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
+}
+
+/**
  * Helper function to make HTTP GET requests.
  *
  * @param url where we send the request
  * @param callback function that is called with the xhr responseText
  */
 function _httpGetRequest(url, callback) {
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
   try {
@@ -222,12 +311,13 @@ Snippets.prototype = {
   },
 
   // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
   notify: function(timer) {
     if (!SNIPPETS_ENABLED) {
       return;
     }
     update();
+    sendStats();
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);