Bug 1292360 - Add attribution data to the telemetry environment block. r=gfritzsche, data-review=bsmedberg, a=ritu
authorMatt Howell <mhowell@mozilla.com>
Wed, 24 Aug 2016 10:01:10 -0700
changeset 350599 7703463c942113771a44a3555309c294c7fbf47d
parent 350598 d557c4b7a35835de19370c95b71b7680097ed371
child 350600 b05f62c12265e38c6f2cd4bc8944645325d51b19
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgfritzsche, ritu
bugs1292360
milestone50.0
Bug 1292360 - Add attribution data to the telemetry environment block. r=gfritzsche, data-review=bsmedberg, a=ritu MozReview-Commit-ID: 4sltJYJrrFS
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/docs/environment.rst
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -19,16 +19,18 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const Utils = TelemetryUtils;
 
+XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
+                                  "resource:///modules/AttributionCode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
 if (AppConstants.platform !== "gonk") {
   Cu.import("resource://gre/modules/AddonManager.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                     "resource://gre/modules/LightweightThemeManager.jsm");
 }
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
@@ -37,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
 
 const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
 
 // The maximum length of a string (e.g. description) in the addons section.
 const MAX_ADDON_STRING_LENGTH = 100;
+// The maximum length of a string value in the settings.attribution object.
+const MAX_ATTRIBUTION_STRING_LENGTH = 100;
 
 /**
  * This is a policy object used to override behavior for testing.
  */
 var Policy = {
   now: () => new Date(),
 };
 
@@ -760,16 +764,17 @@ function EnvironmentCache() {
     };
   } else {
     this._addonBuilder = new EnvironmentAddonBuilder(this);
     p = [ this._addonBuilder.init() ];
   }
 
   this._currentEnvironment.profile = {};
   p.push(this._updateProfile());
+  p.push(this._updateAttribution());
 
   let setup = () => {
     this._initTask = null;
     this._startWatchingPrefs();
     this._addonBuilder.watchForChanges();
     this._updateGraphicsFeatures();
     return this.currentEnvironment;
   };
@@ -1175,16 +1180,31 @@ EnvironmentCache.prototype = {
       Utils.millisecondsToDays(creationDate);
     if (resetDate) {
       this._currentEnvironment.profile.resetDate =
         Utils.millisecondsToDays(resetDate);
     }
   }),
 
   /**
+   * Update the cached attribution data object.
+   * @returns Promise<> resolved when the I/O is complete.
+   */
+  _updateAttribution: Task.async(function* () {
+    let data = yield AttributionCode.getAttrDataAsync();
+    if (Object.keys(data).length > 0) {
+      this._currentEnvironment.settings.attribution = {};
+      for (let key in data) {
+        this._currentEnvironment.settings.attribution[key] =
+          limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH);
+      }
+    }
+  }),
+
+  /**
    * Get the partner data in object form.
    * @return Object containing the partner data.
    */
   _getPartner: function () {
     let partnerData = {
       distributionId: Preferences.get(PREF_DISTRIBUTION_ID, null),
       distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
       partnerId: Preferences.get(PREF_PARTNER_ID, null),
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -54,16 +54,23 @@ Structure::
         },
         userPrefs: {
           // Only prefs which are changed from the default value are listed
           // in this block
           "pref.name.value": value // some prefs send the value
           "pref.name.url": "<user-set>" // For some privacy-sensitive prefs
             // only the fact that the value has been changed is recorded
         },
+        attribution: { // optional, only present if the installation has attribution data
+          // all of these values are optional.
+          source: <string>, // referring partner domain, when install happens via a known partner
+          medium: <string>, // category of the source, such as "organic" for a search engine
+          campaign: <string>, // identifier of the particular campaign that led to the download of the product
+          content: <string>, // identifier to indicate the particular link within a campaign
+        },
       },
       profile: {
         creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
         resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
       },
       partner: { // This section may not be immediately available on startup
         distributionId: <string>, // pref "distribution.id", null on failure
         distributionVersion: <string>, // pref "distribution.version", null on failure
@@ -311,16 +318,25 @@ The following is a partial list of colle
 - ``browser.urlbar.suggest.searches``: True if search suggestions are enabled in the urlbar. Defaults to false.
 
 - ``browser.urlbar.userMadeSearchSuggestionsChoice``: True if the user has clicked Yes or No in the urlbar's opt-in notification. Defaults to false.
 
 - ``browser.zoom.full``: True if zoom is enabled for both text and images, that is if "Zoom Text Only" is not enabled. Defaults to true. Collection of this preference has been enabled in Firefox 50 and will be disabled again in Firefox 53 (`Bug 979323 <https://bugzilla.mozilla.org/show_bug.cgi?id=979323>`_).
 
 - ``security.sandbox.content.level``: The meanings of the values are OS dependent, but 0 means not sandboxed for all OS. Details of the meanings can be found in the `Firefox prefs file <http://hg.mozilla.org/mozilla-central/file/tip/browser/app/profile/firefox.js>`_.
 
+attribution
+~~~~~~~~~~~
+
+This object contains the attribution data for the product installation.
+
+Attribution data is used to link installations of Firefox with the source that the user arrived at the Firefox download page from. It would indicate, for instance, when a user executed a web search for Firefox and arrived at the download page from there, directly navigated to the site, clicked on a link from a particular social media campaign, etc.
+
+The attribution data is included in some versions of the default Firefox installer for Windows (the "stub" installer) and stored as part of the installation. All platforms other than Windows and also Windows installations that did not use the stub installer do not have this data and will not include the ``attribution`` object.
+
 partner
 -------
 
 If the user is using a partner repack, this contains information identifying the repack being used, otherwise "partnerNames" will be an empty array and other entries will be null. The information may be missing when the profile just becomes available. In Firefox for desktop, the information along with other customizations defined in distribution.ini are processed later in the startup phase, and will be fully applied when "distribution-customization-complete" notification is sent.
 
 Distributions are most reliably identified by the ``distributionId`` field. Partner information can be found in the `partner repacks <https://github.com/mozilla-partners>`_ (`the old one <http://hg.mozilla.org/build/partner-repacks/>`_ is deprecated): it contains one private repository per partner.
 Important values for ``distributionId`` include:
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource:///modules/AttributionCode.jsm");
 Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm", this);
 Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://testing-common/AddonManagerTesting.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://testing-common/MockRegistrar.jsm", this);
 Cu.import("resource://gre/modules/FileUtils.jsm");
@@ -66,16 +67,19 @@ const PERSONA_ID_SUFFIX = "@personas.moz
 const PERSONA_NAME = "Test Theme";
 const PERSONA_DESCRIPTION = "A nice theme/persona description.";
 
 const PLUGIN_UPDATED_TOPIC     = "plugins-list-updated";
 
 // system add-ons are enabled at startup, so record date when the test starts
 const SYSTEM_ADDON_INSTALL_DATE = Date.now();
 
+// Valid attribution code to write so that settings.attribution can be tested.
+const ATTRIBUTION_CODE = "source%3Dgoogle.com";
+
 /**
  * Used to mock plugin tags in our fake plugin host.
  */
 function PluginTag(aName, aDescription, aVersion, aEnabled) {
   this.name = aName;
   this.description = aDescription;
   this.version = aVersion;
   this.disabled = !aEnabled;
@@ -263,16 +267,41 @@ function spoofPartnerInfo() {
   prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;
 
   // Spoof the preferences.
   for (let pref in prefsToSpoof) {
     Preferences.set(pref, prefsToSpoof[pref]);
   }
 }
 
+function getAttributionFile() {
+  let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
+  file.append("mozilla");
+  file.append(AppConstants.MOZ_APP_NAME);
+  file.append("postSigningData");
+  return file;
+}
+
+function spoofAttributionData() {
+  if (gIsWindows) {
+    AttributionCode._clearCache();
+    let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+                 createInstance(Ci.nsIFileOutputStream);
+    stream.init(getAttributionFile(), -1, -1, 0);
+    stream.write(ATTRIBUTION_CODE, ATTRIBUTION_CODE.length);
+  }
+}
+
+function cleanupAttributionData() {
+  if (gIsWindows) {
+    getAttributionFile().remove(false);
+    AttributionCode._clearCache();
+  }
+}
+
 /**
  * Check that a value is a string and not empty.
  *
  * @param aValue The variable to check.
  * @return True if |aValue| has type "string" and is not empty, False otherwise.
  */
 function checkString(aValue) {
   return (typeof aValue == "string") && (aValue != "");
@@ -377,16 +406,21 @@ function checkSettingsSection(data) {
   Assert.equal(typeof update.enabled, "boolean");
   Assert.equal(typeof update.autoDownload, "boolean");
 
   // Check "defaultSearchEngine" separately, as it can either be undefined or string.
   if ("defaultSearchEngine" in data.settings) {
     checkString(data.settings.defaultSearchEngine);
     Assert.equal(typeof data.settings.defaultSearchEngineData, "object");
   }
+
+  if ("attribution" in data.settings) {
+    Assert.equal(typeof data.settings.attribution, "object");
+    Assert.equal(data.settings.attribution.source, "google.com");
+  }
 }
 
 function checkProfileSection(data) {
   Assert.ok("profile" in data, "There must be a profile section in Environment.");
   Assert.equal(data.profile.creationDate, truncateToDays(PROFILE_CREATION_DATE_MS));
   Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS));
 }
 
@@ -762,16 +796,20 @@ function run_test() {
   gHttpRoot = "http://localhost:" + port + "/";
   gDataRoot = gHttpRoot + "data/";
   gHttpServer.registerDirectory("/data/", do_get_cwd());
   do_register_cleanup(() => gHttpServer.stop(() => {}));
 
   // Spoof the the hotfixVersion
   Preferences.set("extensions.hotfix.lastVersion", APP_HOTFIX_VERSION);
 
+  // Create the attribution data file, so that settings.attribution will exist.
+  spoofAttributionData();
+  do_register_cleanup(cleanupAttributionData);
+
   run_next_test();
 }
 
 function isRejected(promise) {
   return new Promise((resolve, reject) => {
     promise.then(() => resolve(false), () => resolve(true));
   });
 }