[Pkg-mozext-commits] [requestpolicy] 04/08: Added telemetry for research study.

David Prévot taffit at moszumanska.debian.org
Fri Sep 19 17:44:28 UTC 2014


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to annotated tag release-0.5.27
in repository requestpolicy.

commit c2d5b030c5bf674d2e134f4a2407e4a21255217d
Author: Justin Samuel <js at justinsamuel.com>
Date:   Fri Dec 2 18:57:33 2011 -0800

    Added telemetry for research study.
---
 src/components/requestpolicyService.js |  155 +++-
 src/content/consentForm.html           |  163 ++++
 src/content/menu.js                    |   22 +
 src/content/overlay.js                 |   97 ++-
 src/content/overlay.xul                |    9 +
 src/content/prefWindow.js              |   29 +
 src/defaults/preferences/defaults.js   |    7 +
 src/modules/DomainUtil.jsm             |   13 +
 src/modules/FileUtil.jsm               |  111 ++-
 src/modules/Services.jsm               |   30 +
 src/modules/Stats.jsm                  | 1270 ++++++++++++++++++++++++++++++++
 src/modules/Telemetry.jsm              |  217 ++++++
 src/skin/requestpolicy.css             |    8 +
 13 files changed, 2120 insertions(+), 11 deletions(-)

diff --git a/src/components/requestpolicyService.js b/src/components/requestpolicyService.js
index b8abca3..062a1e9 100644
--- a/src/components/requestpolicyService.js
+++ b/src/components/requestpolicyService.js
@@ -382,6 +382,12 @@ RequestPolicyService.prototype = {
     }
   },
 
+  _clearPref: function(name) {
+    if (this.prefs.prefHasUserValue(name)) {
+      this.prefs.clearUserPref(name);
+    }
+  },
+
   _syncFromPrefs : function() {
     // Load the logging preferences before the others.
     this._updateLoggingSettings();
@@ -430,18 +436,99 @@ RequestPolicyService.prototype = {
       }
     }
 
-    // Clean up old, unused prefs (removed in 0.2.0).
-    deletePrefs = ["temporarilyAllowedOrigins",
-        "temporarilyAllowedDestinations",
-        "temporarilyAllowedOriginsToDestinations"];
-    for (var i = 0; i < deletePrefs.length; i++) {
-      if (this.prefs.prefHasUserValue(deletePrefs[i])) {
-        this.prefs.clearUserPref(deletePrefs[i]);
+    if (requestpolicy.mod.Telemetry.isPastEndDate()) {
+      if (this.prefs.getBoolPref('study.participate')) {
+        this.endParticipationInStudy();
+      }
+      this._clearPref('study.participate');
+      this._clearPref('study.profileID');
+      this._clearPref('study.consentID');
+      this._clearPref('study.consentVersion');
+      this._clearPref('study.sessionID');
+      this._clearPref('study.globalEventID');
+      this._prefService.savePrefFile(null);
+    } else if (this.prefs.getBoolPref('study.participate')) {
+      var profileID = this.prefs.getCharPref('study.profileID');
+      profileID = parseInt(profileID);
+      // If participating in the study there should already be a profileID.
+      if (!profileID) {
+        this.prefs.setBoolPref('study.participate', false);
+        this._prefService.savePrefFile(null);
+        return;
       }
+      requestpolicy.mod.Telemetry.setProfileID(profileID);
+
+      var consentID = this.prefs.getIntPref('study.consentID');
+      requestpolicy.mod.Telemetry.setConsentID(consentID);
+
+      var sessionID = this.prefs.getIntPref('study.sessionID');
+      sessionID++;
+      this.prefs.setIntPref('study.sessionID', sessionID);
+      requestpolicy.mod.Telemetry.setSessionID(sessionID);
+
+      var globalEventID = this.prefs.getIntPref('study.globalEventID');
+      requestpolicy.mod.Telemetry.setGlobalEventID(globalEventID);
+
+      this._prefService.savePrefFile(null);
+      requestpolicy.mod.Telemetry.setEnabled(true);
     }
+
+    // Clean up old, unused prefs (removed in 0.2.0).
+    this._clearPref("temporarilyAllowedOrigins");
+    this._clearPref("temporarilyAllowedDestinations");
+    this._clearPref("temporarilyAllowedOriginsToDestinations");
     this._prefService.savePrefFile(null);
   },
 
+  beginParticipationInStudy : function() {
+    // I assume I made profileID a char pref rather than an int pref out of
+    // concern that it might be limited to 32-bit values.
+    var profileID = this.prefs.getCharPref('study.profileID');
+    if (!profileID) {
+      profileID = requestpolicy.mod.Telemetry.generateProfileID();
+      this.prefs.setCharPref('study.profileID', profileID);
+    }
+    requestpolicy.mod.Telemetry.setProfileID(profileID);
+
+    var consentID = this.prefs.getIntPref('study.consentID');
+    consentID++;
+    this.prefs.setIntPref('study.consentID', consentID);
+    requestpolicy.mod.Telemetry.setConsentID(consentID);
+
+    this.prefs.setIntPref('study.consentVersion', 1);
+
+    var sessionID = this.prefs.getIntPref('study.sessionID');
+    sessionID++;
+    this.prefs.setIntPref('study.sessionID', sessionID);
+    requestpolicy.mod.Telemetry.setSessionID(sessionID);
+
+    this.prefs.setBoolPref('study.participate', true);
+    this._prefService.savePrefFile(null);
+    requestpolicy.mod.Telemetry.setEnabled(true);
+
+    requestpolicy.mod.Stats.consentGranted();
+
+    // We have to process the queue synchronously here (or not at all) because
+    // doing it async causes the xhr request to silently disappear and the
+    // events are lost.
+    //requestpolicy.mod.Telemetry.processQueue(null, true);
+  },
+
+  endParticipationInStudy : function() {
+    requestpolicy.mod.Stats.consentRevoked();
+    requestpolicy.mod.Telemetry.processQueue();
+
+    this.prefs.setBoolPref('study.participate', false);
+    this._clearPref('study.consentVersion');
+    this._prefService.savePrefFile(null);
+    requestpolicy.mod.Telemetry.setEnabled(false);
+    requestpolicy.mod.Stats.deleteFile();
+  },
+
+  isParticipatingInStudy : function() {
+    return requestpolicy.mod.Telemetry.getEnabled();
+  },
+
   _updateLoggingSettings : function() {
     requestpolicy.mod.Logger.enabled = this.prefs.getBoolPref("log");
     requestpolicy.mod.Logger.level = this.prefs.getIntPref("log.level");
@@ -606,6 +693,7 @@ RequestPolicyService.prototype = {
       default :
         break;
     }
+    requestpolicy.mod.Stats.prefChanged(prefName);
   },
 
   _loadLibraries : function() {
@@ -615,6 +703,10 @@ RequestPolicyService.prototype = {
         requestpolicy.mod);
     Components.utils.import("resource://requestpolicy/Util.jsm",
         requestpolicy.mod);
+    Components.utils.import("resource://requestpolicy/Stats.jsm",
+        requestpolicy.mod);
+    Components.utils.import("resource://requestpolicy/Telemetry.jsm",
+        requestpolicy.mod);
     try {
       Components.utils.import("resource://gre/modules/AddonManager.jsm");
     } catch (e) {
@@ -1104,6 +1196,7 @@ RequestPolicyService.prototype = {
     if (!noStore) {
       this._storePreferenceList("allowedOrigins");
     }
+    requestpolicy.mod.Stats.ruleAddedOrigin(host);
   },
 
   allowOrigin : function allowOrigin(host) {
@@ -1123,6 +1216,7 @@ RequestPolicyService.prototype = {
       this._temporarilyAllowedOriginsCount++;
       this._temporarilyAllowedOrigins[host] = true;
     }
+    requestpolicy.mod.Stats.ruleAddedTempOrigin(host);
   },
 
   isTemporarilyAllowedOrigin : function isTemporarilyAllowedOrigin(host) {
@@ -1134,6 +1228,7 @@ RequestPolicyService.prototype = {
     if (!noStore) {
       this._storePreferenceList("allowedDestinations");
     }
+    requestpolicy.mod.Stats.ruleAddedDest(host);
   },
 
   allowDestination : function allowDestination(host) {
@@ -1153,6 +1248,7 @@ RequestPolicyService.prototype = {
       this._temporarilyAllowedDestinationsCount++;
       this._temporarilyAllowedDestinations[host] = true;
     }
+    requestpolicy.mod.Stats.ruleAddedTempDest(host);
   },
 
   isTemporarilyAllowedDestination : function isTemporarilyAllowedDestination(
@@ -1181,6 +1277,8 @@ RequestPolicyService.prototype = {
     var combinedId = this._getCombinedOriginToDestinationIdentifier(
         originIdentifier, destIdentifier);
     this._allowOriginToDestinationByCombinedIdentifier(combinedId, noStore);
+    requestpolicy.mod.Stats.ruleAddedOriginToDest(
+      originIdentifier, destIdentifier);
   },
 
   allowOriginToDestination : function allowOriginToDestination(
@@ -1215,6 +1313,8 @@ RequestPolicyService.prototype = {
       this._temporarilyAllowedOriginsToDestinationsCount++;
       this._temporarilyAllowedOriginsToDestinations[combinedId] = true;
     }
+    requestpolicy.mod.Stats.ruleAddedTempOriginToDest(
+      originIdentifier, destIdentifier);
   },
 
   isTemporarilyAllowedOriginToDestination : function isTemporarilyAllowedOriginToDestination(
@@ -1237,18 +1337,21 @@ RequestPolicyService.prototype = {
     this._temporarilyAllowedOriginsToDestinations = {};
 
     this._blockingDisabled = false;
+    // TODO: stats: remove all temp rules
   },
 
   _forbidOrigin : function(host, noStore) {
     if (this._temporarilyAllowedOrigins[host]) {
       this._temporarilyAllowedOriginsCount--;
       delete this._temporarilyAllowedOrigins[host];
+      requestpolicy.mod.Stats.ruleRemovedTempOrigin(host);
     }
     if (this._allowedOrigins[host]) {
       delete this._allowedOrigins[host];
       if (!noStore) {
         this._storePreferenceList("allowedOrigins");
       }
+      requestpolicy.mod.Stats.ruleRemovedOrigin(host);
     }
   },
 
@@ -1264,12 +1367,14 @@ RequestPolicyService.prototype = {
     if (this._temporarilyAllowedDestinations[host]) {
       this._temporarilyAllowedDestinationsCount--;
       delete this._temporarilyAllowedDestinations[host];
+      requestpolicy.mod.Stats.ruleRemovedTempDest(host);
     }
     if (this._allowedDestinations[host]) {
       delete this._allowedDestinations[host];
       if (!noStore) {
         this._storePreferenceList("allowedDestinations");
       }
+      requestpolicy.mod.Stats.ruleRemovedDest(host);
     }
   },
 
@@ -1299,15 +1404,22 @@ RequestPolicyService.prototype = {
   },
 
   _forbidOriginToDestinationByCombinedIdentifier : function(combinedId, noStore) {
+    var parts = combinedId.split('|');
+    var originIdentifier = parts[0];
+    var destIdentifier = parts[1];
     if (this._temporarilyAllowedOriginsToDestinations[combinedId]) {
       this._temporarilyAllowedOriginsToDestinationsCount--;
       delete this._temporarilyAllowedOriginsToDestinations[combinedId];
+      requestpolicy.mod.Stats.ruleRemovedTempOriginToDest(
+        originIdentifier, destIdentifier);
     }
     if (this._allowedOriginsToDestinations[combinedId]) {
       delete this._allowedOriginsToDestinations[combinedId];
       if (!noStore) {
         this._storePreferenceList("allowedOriginsToDestinations");
       }
+      requestpolicy.mod.Stats.ruleRemovedOriginToDest(
+        originIdentifier, destIdentifier);
     }
   },
 
@@ -1643,6 +1755,8 @@ RequestPolicyService.prototype = {
         // what is needed to allow their requests.
         this._initializeExtensionCompatibility();
         this._initializeApplicationCompatibility();
+
+        requestpolicy.mod.Stats.browserStarted();
         break;
       case "private-browsing" :
         if (data == "enter") {
@@ -1682,6 +1796,15 @@ RequestPolicyService.prototype = {
         if (this._uninstall) {
           this._handleUninstallOrDisable();
         }
+        requestpolicy.mod.Stats.reportQueuedDocActions();
+        requestpolicy.mod.Stats.reportStatsHighFrequency();
+        requestpolicy.mod.Stats.reportStatsMedFrequency();
+        // Don't send the low frequency stats because some of them (e.g. addons)
+        // happen async.
+        //requestpolicy.mod.Stats.reportStatsLowFrequency();
+        requestpolicy.mod.Telemetry.track('browser_quit');
+        requestpolicy.mod.Telemetry.processQueue(null, true);
+        requestpolicy.mod.Stats.saveToFile();
         break;
       default :
         requestpolicy.mod.Logger.warning(requestpolicy.mod.Logger.TYPE_ERROR,
@@ -2110,6 +2233,11 @@ RequestPolicyService.prototype = {
         var args = [aContentType, dest, origin, aContext, aMimeTypeGuess,
             aExtra];
 
+        // TYPE_DOCUMENT = 6
+        if (aContentType == 6) {
+          requestpolicy.mod.Stats.requestedTopLevelDocument(dest);
+        }
+
         if (aContext && aContext.nodeName == "LINK" &&
             (aContext.rel == "icon" || aContext.rel == "shortcut icon")) {
           this._faviconRequests[dest] = true;
@@ -2127,6 +2255,7 @@ RequestPolicyService.prototype = {
         // regardless of the link click. The original tab would then show it
         // in its menu.
         if (this._clickedLinks[origin] && this._clickedLinks[origin][dest]) {
+          requestpolicy.mod.Stats.allowedLinkClick(args);
           // Don't delete the _clickedLinks item. We need it for if the user
           // goes back/forward through their history.
           // delete this._clickedLinks[origin][dest];
@@ -2135,6 +2264,7 @@ RequestPolicyService.prototype = {
 
         } else if (this._submittedForms[origin]
             && this._submittedForms[origin][dest.split("?")[0]]) {
+          requestpolicy.mod.Stats.allowedFormSubmission(args);
           // Note: we dropped the query string from the dest because form GET
           // requests will have that added on here but the original action of
           // the form may not have had it.
@@ -2162,32 +2292,41 @@ RequestPolicyService.prototype = {
         var destIdentifier = this.getUriIdentifier(dest);
 
         if (destIdentifier == originIdentifier) {
+          requestpolicy.mod.Stats.allowedSameHost(args);
           return this.accept("same host (at current domain strictness level)",
               args);
         }
 
         if (this.isAllowedOriginToDestination(originIdentifier, destIdentifier)) {
+          requestpolicy.mod.Stats.allowedByOriginToDest(
+            args, originIdentifier, destIdentifier);
           return this.accept("Allowed origin to destination", args);
         }
 
         if (this.isAllowedOrigin(originIdentifier)) {
+          requestpolicy.mod.Stats.allowedByOrigin(args, originIdentifier);
           return this.accept("Allowed origin", args);
         }
 
         if (this.isAllowedDestination(destIdentifier)) {
+          requestpolicy.mod.Stats.allowedByDest(args, destIdentifier);
           return this.accept("Allowed destination", args);
         }
 
         if (this.isTemporarilyAllowedOriginToDestination(originIdentifier,
             destIdentifier)) {
+          requestpolicy.mod.Stats.tempAllowedByOriginToDest(
+            args, originIdentifier, destIdentifier);
           return this.accept("Temporarily allowed origin to destination", args);
         }
 
         if (this.isTemporarilyAllowedOrigin(originIdentifier)) {
+          requestpolicy.mod.Stats.tempAllowedByOrigin(args, originIdentifier);
           return this.accept("Temporarily allowed origin", args);
         }
 
         if (this.isTemporarilyAllowedDestination(destIdentifier)) {
+          requestpolicy.mod.Stats.tempAllowedByDest(args, destIdentifier);
           return this.accept("Temporarily allowed destination", args);
         }
 
@@ -2264,6 +2403,8 @@ RequestPolicyService.prototype = {
           }
         }
 
+        requestpolicy.mod.Stats.denied(args);
+
         // We didn't match any of the conditions in which to allow the request,
         // so reject it.
         return aExtra == CP_MAPPEDDESTINATION ? CP_REJECT :
diff --git a/src/content/consentForm.html b/src/content/consentForm.html
new file mode 100644
index 0000000..0c6800d
--- /dev/null
+++ b/src/content/consentForm.html
@@ -0,0 +1,163 @@
+<html>
+<style>
+  body {
+    margin: 0 auto 0 auto;
+    font-family: Verdana, sans-serif;
+    font-size: 87%;
+    max-width: 870px;
+    background-color: #e4e4e4;
+  }
+  #container {
+    margin: 20px;
+    padding: 30px;
+    background-color: #fff;
+    border: 1px solid #999;
+    border-radius: 10px;
+  }
+  #header {
+    margin-bottom: 2em;
+    text-align: center;
+  }
+  #header #ucb {
+    font-size: 1.2em;
+  }
+  #header #purpose {
+    font-size: 1em;
+  }
+  #header h1 {
+    font-size: 1.4em;
+  }
+  #note {
+    font-size: 1.2em;
+    border: 1px solid #d1e2d1;
+    border-radius: 10px;
+    background-color: #e1f2e1;
+    margin-bottom: 2em;
+    text-align: center;
+    font-style: italic;
+    color: #006b10;
+    padding: 1em;
+  }
+  .section {
+    margin-top: 1em;
+  }
+  .section h2 {
+    font-size: 1.2em;
+    margin-bottom: 0.8em;
+    border-bottom: 1px solid #ddd;
+  }
+  div#accept {
+    text-align: center;
+    margin-top: 2em;
+  }
+  div#accept p {
+    font-size : 1.1em;
+    margin : 0 4em 1em 4em;
+    text-align : center;
+    font-weight : bold;
+  }
+  div#accept button {
+    font-size: 1.4em;
+  }
+</style>
+<script>
+  function acceptForm() {
+    var rp = Components.classes["@requestpolicy.com/requestpolicy-service;1"]
+            .getService(Components.interfaces.nsIRequestPolicy).wrappedJSObject;
+    rp.beginParticipationInStudy();
+    window.close();
+  }
+</script>
+<body>
+
+<div id="container">
+
+  <div id="header">
+    <div id="ucb">University of California at Berkeley</div>
+    <div id="purpose">Consent to Participate in Research</div>
+    <h1>Understanding Usage of RequestPolicy through Telemetry</h1>
+  </div>
+
+  <div id="note">
+    In order to participate, you must be at least 18 years old and click "Accept" at the bottom of this page.
+  </div>
+
+  <div class="section">
+    <h2>Introduction and Purpose</h2>
+    My name is Justin Samuel. I'm the author of RequestPolicy. I'm also a graduate student at the University of
+    California, Berkeley working with Professor Vern Paxson in the Department of Electrical Engineering and Computer
+    Sciences. I would like to invite you to take part in my research study of the usage of RequestPolicy.
+  </div>
+
+  <div class="section">
+    <h2>Procedures</h2>
+    If you agree to participate in my research, RequestPolicy will collect and transmit back to us some data about how
+    you use RequestPolicy. We will not collect information about specific websites you visit. The type of information
+    we're collecting involves how you use RequestPolicy and how RequestPolicy impacts your browsing. For example, we
+    will collect information on how often you add and remove items from your whitelist (but not what those items are),
+    whether you use certain features, which toolbar you have the RequestPolicy button placed in, how long RequestPolicy
+    takes to load your whitelist when the browser starts, and how many items you have in your whitelist.
+  </div>
+
+  <div class="section">
+    <h2>Benefits</h2>
+    There is no direct benefit to you from taking part in this study. We hope that the research will improve the
+    usability of RequestPolicy as well as other privacy and security software.
+  </div>
+
+  <div class="section">
+    <h2>Risks</h2>
+    As with all research, there is a chance that confidentiality could be compromised. However, we are taking
+    precautions to minimize this risk.
+  </div>
+
+  <div class="section">
+    <h2>Confidentiality</h2>
+    Your study data will be handled as confidentially as possible. If results of this study are published or presented,
+    personally identifiable information will not be used. We will not record your IP address on our server when we
+    collect study data.
+
+    To further decrease risks to confidentiality, RequestPolicy will use SSL encryption when it transmits study data
+    back to our server. Of course, we'll also do our best to keep our server secure.
+
+    When the research is completed, I may save the data for use in future research done by myself or others. The data
+    may also be used by future RequestPolicy developers other than myself. We will not make public the data that we
+    collect.
+  </div>
+
+  <div class="section">
+    <h2>Compensation</h2>
+    You will not be paid for taking part in this study.
+  </div>
+
+  <div class="section">
+    <h2>Rights</h2>
+    Participation in research is completely voluntary.  You have the right to decline to participate or to withdraw at any point in this study without penalty or loss of benefits to which you are otherwise entitled.
+  </div>
+
+  <div class="section">
+    <h2>Questions</h2>
+    If you have any questions about this research, please feel free to contact me. I can be reached at
+    support at requestpolicy.com.
+
+    If you have any questions about your rights or treatment as a research participant in this study, please contact the
+    University of California at Berkeley's Committee for Protection of Human Subjects at 510-642-7461, or e-mail
+    subjects at berkeley.edu.
+
+    If you agree to take part in the research, please click the "Accept" button below.
+  </div>
+
+  <div id="accept">
+    <p>I certify that I am 18 years or older. I have read this consent form and I agree to take part in this research.</p>
+
+    <noscript>
+      JavaScript is necessary to submit this form.
+    </noscript>
+
+    <button onclick="acceptForm()">Accept</button>
+  </div>
+
+</div>
+
+</body>
+</html>
diff --git a/src/content/menu.js b/src/content/menu.js
index fe9634e..965b3f5 100644
--- a/src/content/menu.js
+++ b/src/content/menu.js
@@ -30,6 +30,8 @@ Components.utils.import("resource://requestpolicy/Logger.jsm",
     requestpolicy.mod);
 Components.utils.import("resource://requestpolicy/RequestUtil.jsm",
     requestpolicy.mod);
+Components.utils.import("resource://requestpolicy/Telemetry.jsm",
+    requestpolicy.mod);
 
 requestpolicy.menu = {
 
@@ -70,6 +72,10 @@ requestpolicy.menu = {
   _itemForbidOrigin : null,
   _itemUnrestrictedOrigin : null,
 
+  _itemParticipateInStudy : null,
+  _itemEndParticipationInStudy : null,
+  _itemStudySeparator : null,
+
   init : function() {
     if (this._initialized == false) {
       this._initialized = true;
@@ -125,6 +131,13 @@ requestpolicy.menu = {
       this._itemUnrestrictedOrigin = document
           .getElementById("requestpolicyUnrestrictedOrigin");
 
+      this._itemParticipateInStudy = document
+        .getElementById("requestpolicyParticipateInStudy");
+      this._itemEndParticipationInStudy = document
+        .getElementById("requestpolicyEndParticipationInStudy");
+      this._itemStudySeparator = document
+          .getElementById("requestpolicy-studySeparator");
+
       var conflictCount = this._rpServiceJSObject.getConflictingExtensions().length;
       var hideConflictInfo = (conflictCount == 0);
       if (!hideConflictInfo) {
@@ -172,6 +185,13 @@ requestpolicy.menu = {
       this._itemPrefetchWarning.hidden = hidePrefetchInfo;
       this._itemPrefetchWarningSeparator.hidden = hidePrefetchInfo;
 
+      var studyEnded = requestpolicy.mod.Telemetry.isPastEndDate();
+      this._itemParticipateInStudy.hidden =
+        studyEnded || this._rpServiceJSObject.isParticipatingInStudy();
+      this._itemEndParticipationInStudy.hidden =
+        studyEnded || !this._rpServiceJSObject.isParticipatingInStudy();
+      this._itemStudySeparator.hidden = studyEnded;
+
       if (isChromeUri) {
         this._itemUnrestrictedOrigin.setAttribute("label", this._strbundle
                 .getFormattedString("unrestrictedOrigin", ["chrome://"]));
@@ -308,6 +328,8 @@ requestpolicy.menu = {
       // origins" item in the main menu.
       this._itemOtherOrigins.hidden = this._itemOtherOriginsSeparator.hidden = (otherOriginMenuCount == 0);
 
+      this.stats_otherOriginMenuCount = otherOriginMenuCount;
+
     } catch (e) {
       requestpolicy.mod.Logger.severe(requestpolicy.mod.Logger.TYPE_ERROR,
           "Fatal Error, " + e + ", stack was: " + e.stack);
diff --git a/src/content/overlay.js b/src/content/overlay.js
index 056dec5..d190400 100644
--- a/src/content/overlay.js
+++ b/src/content/overlay.js
@@ -34,6 +34,8 @@ Components.utils.import("resource://requestpolicy/Logger.jsm",
     requestpolicy.mod);
 Components.utils.import("resource://requestpolicy/RequestUtil.jsm",
     requestpolicy.mod);
+Components.utils.import("resource://requestpolicy/Stats.jsm",
+    requestpolicy.mod);
 Components.utils.import("resource://requestpolicy/Util.jsm",
     requestpolicy.mod);
 
@@ -1279,6 +1281,17 @@ requestpolicy.overlay = {
       return;
     }
     requestpolicy.menu.prepareMenu();
+    try {
+      // TODO: include details of what's in the menu
+      var blockedDests = requestpolicy.menu._blockedDestinationsItems.length;
+      var allowedDests = requestpolicy.menu._allowedDestinationsItems.length;
+      var otherOrigins = requestpolicy.menu.stats_otherOriginMenuCount;
+      requestpolicy.mod.Logger.dump('otherOrigins: ' + otherOrigins);
+      requestpolicy.mod.Stats.menuOpened(
+        this.getTopLevelDocumentUri(), blockedDests, allowedDests, otherOrigins);
+    } catch (e) {
+      requestpolicy.mod.Logger.dump('error calling Stats.menuOpened: ' + e);
+    }
   },
 
   /**
@@ -1291,9 +1304,10 @@ requestpolicy.overlay = {
     if (event.currentTarget != event.originalTarget) {
       return;
     }
-    // Leave the popup attached to the context menu, as we consdier that the
+    // Leave the popup attached to the context menu, as we consider that the
     // default location for it.
     this._attachPopupToContextMenu();
+    requestpolicy.mod.Stats.menuClosed(this.getTopLevelDocumentUri());
   },
 
   /**
@@ -1350,6 +1364,15 @@ requestpolicy.overlay = {
     // would be unexpected to the user if all were reloaded.
     this
         ._setPermissiveNotificationForAllWindows(this._rpServiceJSObject._blockingDisabled);
+
+    if (this._rpServiceJSObject._blockingDisabled) {
+      requestpolicy.mod.Stats.menuActionTempAllowAllEnabled(
+        this.getTopLevelDocumentUri());
+    } else {
+      requestpolicy.mod.Stats.menuActionTempAllowAllDisabled(
+        this.getTopLevelDocumentUri());
+    }
+
     this._conditionallyReloadDocument();
   },
 
@@ -1359,6 +1382,12 @@ requestpolicy.overlay = {
    */
   temporarilyAllowOrigin : function(originHost) {
     this._rpService.temporarilyAllowOrigin(originHost);
+
+    // TODO: should we indicate in stats that this is for an "other origin"?
+    // If so, we should be tracking that for other menu selections, as well.
+    requestpolicy.mod.Stats.menuActionTempAllowOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1374,6 +1403,10 @@ requestpolicy.overlay = {
     // "window.target".
     var host = this.getTopLevelDocumentUriIdentifier();
     this._rpService.temporarilyAllowOrigin(host);
+
+    requestpolicy.mod.Stats.menuActionTempAllowOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1386,6 +1419,10 @@ requestpolicy.overlay = {
    */
   temporarilyAllowDestination : function(destHost) {
     this._rpService.temporarilyAllowDestination(destHost);
+
+    requestpolicy.mod.Stats.menuActionTempAllowDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1400,6 +1437,10 @@ requestpolicy.overlay = {
    */
   temporarilyAllowOriginToDestination : function(originHost, destHost) {
     this._rpService.temporarilyAllowOriginToDestination(originHost, destHost);
+
+    requestpolicy.mod.Stats.menuActionTempAllowOriginToDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1408,6 +1449,10 @@ requestpolicy.overlay = {
    */
   allowOrigin : function(originHost) {
     this._rpService.allowOrigin(originHost);
+
+    requestpolicy.mod.Stats.menuActionAllowOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1421,6 +1466,10 @@ requestpolicy.overlay = {
   allowCurrentOrigin : function(event) {
     var host = this.getTopLevelDocumentUriIdentifier();
     this._rpService.allowOrigin(host);
+
+    requestpolicy.mod.Stats.menuActionAllowOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1432,6 +1481,10 @@ requestpolicy.overlay = {
    */
   allowDestination : function(destHost) {
     this._rpService.allowDestination(destHost);
+
+    requestpolicy.mod.Stats.menuActionAllowDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1446,6 +1499,10 @@ requestpolicy.overlay = {
    */
   allowOriginToDestination : function(originHost, destHost) {
     this._rpService.allowOriginToDestination(originHost, destHost);
+
+    requestpolicy.mod.Stats.menuActionAllowOriginToDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1455,6 +1512,10 @@ requestpolicy.overlay = {
    */
   forbidOrigin : function(originHost) {
     this._rpService.forbidOrigin(originHost);
+
+    requestpolicy.mod.Stats.menuActionForbidOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1469,6 +1530,10 @@ requestpolicy.overlay = {
   forbidCurrentOrigin : function(event) {
     var host = this.getTopLevelDocumentUriIdentifier();
     this._rpService.forbidOrigin(host);
+
+    requestpolicy.mod.Stats.menuActionForbidOrigin(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1481,6 +1546,10 @@ requestpolicy.overlay = {
    */
   forbidDestination : function(destHost) {
     this._rpService.forbidDestination(destHost);
+
+    requestpolicy.mod.Stats.menuActionForbidDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1496,6 +1565,10 @@ requestpolicy.overlay = {
    */
   forbidOriginToDestination : function(originHost, destHost) {
     this._rpService.forbidOriginToDestination(originHost, destHost);
+
+    requestpolicy.mod.Stats.menuActionForbidOriginToDest(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1510,6 +1583,10 @@ requestpolicy.overlay = {
     // Revoking temporary permissions disables permissive mode. This is partly
     // because permissive mode is called "temporarily allow all".
     this._setPermissiveNotification(false);
+
+    requestpolicy.mod.Stats.menuActionRevokeTempPerms(
+      this.getTopLevelDocumentUri());
+
     this._conditionallyReloadDocument();
   },
 
@@ -1562,6 +1639,9 @@ requestpolicy.overlay = {
     window.openDialog("chrome://requestpolicy/content/prefWindow.xul",
         "requestpolicyPreferencesDialogWindow",
         "chrome, close, centerscreen, alwaysRaised");
+
+    requestpolicy.mod.Stats.menuActionPrefsOpened(
+      this.getTopLevelDocumentUri());
   },
 
   _showInitialSetupDialog : function() {
@@ -1620,14 +1700,27 @@ requestpolicy.overlay = {
           "chrome://requestpolicy/content/requestLog.xul");
       requestLog.hidden = requestLogSplitter.hidden = closeRequestLog.hidden = false;
       openRequestLog.hidden = true;
+
+      requestpolicy.mod.Stats.menuActionRequestLogOpened(
+        this.getTopLevelDocumentUri());
     } else {
       requestLogFrame.setAttribute("src", "about:blank");
       requestLog.hidden = requestLogSplitter.hidden = closeRequestLog.hidden = true;
       openRequestLog.hidden = false;
       this.requestLogTreeView = null;
+
+      requestpolicy.mod.Stats.menuActionRequestLogClosed(
+        this.getTopLevelDocumentUri());
     }
-  }
+  },
 
+  openStudyParticipationWindow : function() {
+    this._openInNewTab("chrome://requestpolicy/content/consentForm.html");
+  },
+
+  endParticipationInStudy : function() {
+    this._rpServiceJSObject.endParticipationInStudy();
+  }
 };
 
 // Initialize the requestpolicy.overlay object when the window DOM is loaded.
diff --git a/src/content/overlay.xul b/src/content/overlay.xul
index 8f9be02..15fd030 100644
--- a/src/content/overlay.xul
+++ b/src/content/overlay.xul
@@ -95,6 +95,15 @@
         hidden="true"
         label="&menu.closeRequestLog.label;"
         oncommand="requestpolicy.overlay.toggleRequestLog();" />
+      <menuseparator id="requestpolicy-studySeparator"/>
+        <menuitem
+        id="requestpolicyParticipateInStudy"
+        label="Participate in research study"
+        oncommand="requestpolicy.overlay.openStudyParticipationWindow();" />
+      <menuitem
+        id="requestpolicyEndParticipationInStudy"
+        label="End participation in research study"
+        oncommand="requestpolicy.overlay.endParticipationInStudy();" />
       <menuseparator id="requestpolicy-preferencesSeparator"/>
       <menuitem
         id="requestpolicyExtensionConflictWarning"
diff --git a/src/content/prefWindow.js b/src/content/prefWindow.js
index 26dc259..d515ff9 100644
--- a/src/content/prefWindow.js
+++ b/src/content/prefWindow.js
@@ -32,6 +32,8 @@ Components.utils.import("resource://requestpolicy/Logger.jsm",
     requestpolicy.mod);
 Components.utils.import("resource://requestpolicy/Prompter.jsm",
     requestpolicy.mod);
+Components.utils.import("resource://requestpolicy/Stats.jsm",
+    requestpolicy.mod);
 
 requestpolicy.prefWindow = {
 
@@ -93,16 +95,19 @@ requestpolicy.prefWindow = {
 
       this._originsList.forbid = function(origin) {
         requestpolicy.prefWindow._rpService.forbidOriginDelayStore(origin);
+        requestpolicy.mod.Stats.prefWindowForbidOrigin();
       };
       this._destinationsList.forbid = function(destination) {
         requestpolicy.prefWindow._rpService
             .forbidDestinationDelayStore(destination);
+        requestpolicy.mod.Stats.prefWindowForbidDest();
       };
       this._originsToDestinationsList.forbid = function(originToDestIdentifier) {
         // Third param is "delay store".
         requestpolicy.prefWindow._rpServiceJSObject
             ._forbidOriginToDestinationByCombinedIdentifier(
                 originToDestIdentifier, true);
+        requestpolicy.mod.Stats.prefWindowForbidOriginToDest();
       };
 
       // Each "allow" button has an array of its associated textboxes.
@@ -125,6 +130,7 @@ requestpolicy.prefWindow = {
         // removed.
         requestpolicy.prefWindow._rpServiceJSObject.forbidOrigin(origin);
         requestpolicy.prefWindow._rpServiceJSObject.allowOrigin(origin);
+        requestpolicy.mod.Stats.prefWindowAllowOrigin();
       };
       this._addDestinationButton.allow = function() {
         var dest = requestpolicy.prefWindow._addDestinationButton.textboxes[0].value;
@@ -132,6 +138,7 @@ requestpolicy.prefWindow = {
         // removed.
         requestpolicy.prefWindow._rpServiceJSObject.forbidDestination(dest);
         requestpolicy.prefWindow._rpServiceJSObject.allowDestination(dest);
+        requestpolicy.mod.Stats.prefWindowAllowDest();
       };
       this._addOriginToDestinationButton.allow = function() {
         var origin = requestpolicy.prefWindow._addOriginToDestinationButton.textboxes[0].value;
@@ -142,9 +149,11 @@ requestpolicy.prefWindow = {
             origin, dest);
         requestpolicy.prefWindow._rpServiceJSObject.allowOriginToDestination(
             origin, dest);
+        requestpolicy.mod.Stats.prefWindowAllowOriginToDest();
       };
 
       this._populateWhitelists();
+      requestpolicy.mod.Stats.prefWindowLoaded();
     }
   },
 
@@ -398,6 +407,8 @@ requestpolicy.prefWindow = {
   },
 
   _import : function(file) {
+    requestpolicy.mod.Stats.prefWindowImport();
+
     requestpolicy.mod.Logger.dump("Starting import from " + file.path);
     var groupToFunctionMap = {
       "origins" : "allowOrigin",
@@ -443,6 +454,8 @@ requestpolicy.prefWindow = {
   },
 
   _export : function(file) {
+    requestpolicy.mod.Stats.prefWindowExport();
+
     requestpolicy.mod.Logger.dump("Starting export to " + file.path);
     var lines = [];
     lines.push("[origins]");
@@ -489,3 +502,19 @@ requestpolicy.prefWindow = {
 addEventListener("DOMContentLoaded", function(event) {
       requestpolicy.prefWindow.init();
     }, false);
+
+addEventListener("unload", function(event) {
+  var extra = null;
+  var textboxes = [
+    requestpolicy.prefWindow._addOrigin_originField,
+    requestpolicy.prefWindow._addDestination_destinationField,
+    requestpolicy.prefWindow._addOriginToDestination_originField,
+    requestpolicy.prefWindow._addOriginToDestination_destinationField
+  ];
+  for (var i in textboxes) {
+    if (textboxes[i].value) {
+      extra = {'textRemainingInRuleCreationTextbox': true};
+    }
+  }
+  requestpolicy.mod.Stats.prefWindowClosed(extra);
+}, false);
diff --git a/src/defaults/preferences/defaults.js b/src/defaults/preferences/defaults.js
index 6706388..c713dd7 100644
--- a/src/defaults/preferences/defaults.js
+++ b/src/defaults/preferences/defaults.js
@@ -27,3 +27,10 @@ pref("extensions.requestpolicy.contextMenu", true);
 
 pref("extensions.requestpolicy.lastVersion", "0.0");
 pref("extensions.requestpolicy.lastAppVersion", "0.0");
+
+pref("extensions.requestpolicy.study.participate", false);
+pref("extensions.requestpolicy.study.profileID", "");
+pref("extensions.requestpolicy.study.consentID", 0);
+pref("extensions.requestpolicy.study.consentVersion", 0);
+pref("extensions.requestpolicy.study.sessionID", 0);
+pref("extensions.requestpolicy.study.globalEventID", 0);
diff --git a/src/modules/DomainUtil.jsm b/src/modules/DomainUtil.jsm
index 6013b4d..67ff1b4 100644
--- a/src/modules/DomainUtil.jsm
+++ b/src/modules/DomainUtil.jsm
@@ -150,6 +150,19 @@ DomainUtil.isValidUri = function(uri) {
   }
 };
 
+
+DomainUtil.isIPAddress = function(host) {
+  try {
+    this._eTLDService.getBaseDomainFromHost(host, 0);
+  } catch (e) {
+    if (e.name == 'NS_ERROR_HOST_IS_IP_ADDRESS') {
+      return true;
+    }
+  }
+  return false;
+};
+
+
 /**
  * Returns the domain from a uri string.
  * 
diff --git a/src/modules/FileUtil.jsm b/src/modules/FileUtil.jsm
index ece8ed3..f11d892 100644
--- a/src/modules/FileUtil.jsm
+++ b/src/modules/FileUtil.jsm
@@ -20,7 +20,21 @@
  * ***** END LICENSE BLOCK *****
  */
 
-var EXPORTED_SYMBOLS = ["FileUtil"]
+var EXPORTED_SYMBOLS = ["FileUtil"];
+
+const CI = Components.interfaces;
+const CC = Components.classes;
+
+if (!requestpolicy) {
+  var requestpolicy = {
+    mod : {}
+  };
+}
+
+Components.utils.import("resource://requestpolicy/Services.jsm",
+    requestpolicy.mod);
+
+const REQUESTPOLICY_DIR = "requestpolicy";
 
 var FileUtil = {
 
@@ -45,6 +59,34 @@ var FileUtil = {
   },
 
   /**
+   * Returns the contents of the file as a string.
+   *
+   * @param {nsIFile}
+   *          file
+   */
+  fileToString : function(file) {
+    var stream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+        .createInstance(Components.interfaces.nsIFileInputStream);
+    stream.init(file, 0x01, 0444, 0);
+    stream.QueryInterface(Components.interfaces.nsILineInputStream);
+
+    var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+                  createInstance(Components.interfaces.nsIConverterInputStream);
+    cstream.init(stream, "UTF-8", 0, 0);
+
+    var str = "";
+    var data = {};
+    do {
+      // Read as much as we can and put it in |data.value|.
+      read = cstream.readString(0xffffffff, data);
+      str += data.value;
+    } while (read != 0);
+    cstream.close(); // This closes |fstream|.
+
+    return str;
+  },
+
+  /**
    * Writes each element of an array to a line of a file (truncates the file if
    * it exists, creates it if it doesn't).
    * 
@@ -68,6 +110,71 @@ var FileUtil = {
     }
     cos.close();
     stream.close();
-  }
+  },
 
+  /**
+   * Writes a string to a file (truncates the file if it exists, creates it if
+   * it doesn't).
+   *
+   * @param {String}
+   *          str
+   * @param {nsIFile}
+   *          file
+   */
+  stringToFile : function(str, file) {
+    // TODO: this should probably write to a tmp file and move the file into
+    // place.
+    var stream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+        .createInstance(Components.interfaces.nsIFileOutputStream);
+    // write, create, append on write, truncate
+    stream.init(file, 0x02 | 0x08 | 0x10 | 0x20, -1, 0);
+
+    var cos = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+        .createInstance(Components.interfaces.nsIConverterOutputStream);
+    cos.init(stream, "UTF-8", 4096, 0x0000);
+    cos.writeString(str);
+    cos.close();
+    stream.close();
+  },
+
+  /**
+   * Returns a file object for a path relative to the user's "requestpolicy"
+   * under their profile directory. The "requestpolicy" directory is created if
+   * it doesn't already exist. Each subdir, if specified, is created if it does
+   * not exist.
+   *
+   * @return {nsILocalFile}
+   */
+  getRPUserDir : function(subdir1, subdir2, subdir3) {
+    var profileDir = requestpolicy.mod.Services.directoryService
+          .get("ProfD", CI.nsIFile);
+    var file = profileDir.clone().QueryInterface(CI.nsILocalFile);
+    file.appendRelativePath(REQUESTPOLICY_DIR);
+    if(!file.exists()) {
+      file.create(CI.nsIFile.DIRECTORY_TYPE, 0700);
+    }
+
+    if (subdir1) {
+      file.appendRelativePath(subdir1);
+      if(!file.exists()) {
+        file.create(CI.nsIFile.DIRECTORY_TYPE, 0700);
+      }
+
+      if (subdir2) {
+        file.appendRelativePath(subdir2);
+        if(!file.exists()) {
+          file.create(CI.nsIFile.DIRECTORY_TYPE, 0700);
+        }
+
+        if (subdir3) {
+          file.appendRelativePath(subdir3);
+          if(!file.exists()) {
+            file.create(CI.nsIFile.DIRECTORY_TYPE, 0700);
+          }
+        }
+      }
+    }
+
+    return file;
+  }
 };
diff --git a/src/modules/Services.jsm b/src/modules/Services.jsm
new file mode 100644
index 0000000..2de904c
--- /dev/null
+++ b/src/modules/Services.jsm
@@ -0,0 +1,30 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2011 Justin Samuel
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EXPORTED_SYMBOLS = ["Services"];
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var Services = {};
+
+XPCOMUtils.defineLazyServiceGetter(Services, "directoryService",
+    "@mozilla.org/file/directory_service;1", "nsIProperties");
diff --git a/src/modules/Stats.jsm b/src/modules/Stats.jsm
new file mode 100644
index 0000000..1dea5df
--- /dev/null
+++ b/src/modules/Stats.jsm
@@ -0,0 +1,1270 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2011 Justin Samuel
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EXPORTED_SYMBOLS = ['Stats'];
+
+// The data in the StoredStats object is written to a file so that the
+// information is available across sessions. This is the filename in the
+// {PROFILE}/requestpolicy/ directory that is used.
+const STORED_STATS_FILENAME = 'telemetry-study.json';
+
+const TYPE_ALLOWED_SAME_HOST = 1;
+const TYPE_ALLOWED_LINK_CLICK = 2;
+const TYPE_ALLOWED_FORM_SUBMISSION = 3;
+const TYPE_ALLOWED_RULE_ORIGIN_TO_DEST = 4;
+const TYPE_ALLOWED_RULE_ORIGIN = 5;
+const TYPE_ALLOWED_RULE_DEST = 6;
+const TYPE_ALLOWED_RULE_TEMP_ORIGIN_TO_DEST = 7;
+const TYPE_ALLOWED_RULE_TEMP_ORIGIN = 8;
+const TYPE_ALLOWED_RULE_TEMP_DEST = 9;
+const TYPE_DENIED = 10;
+
+// How often to send a 'doc' event if there is anything to send.
+const STATS_REPORT_INTERVAL_DOC = 10 * 60 * 1000;
+
+const STATS_REPORT_INTERVAL_HIGH_FREQUENCY = 3600 * 1 * 1000;
+
+const STATS_REPORT_INTERVAL_MED_FREQUENCY = 3600 * 6 * 1000;
+
+// The low frequency report mostly serves to send stats that otherwise are only
+// be sent on browser startup. For users who don't very rarely restart their
+// browser, this is how we get a new report of those stats.
+const STATS_REPORT_INTERVAL_LOW_FREQUENCY = 3600 * 24 * 1000;
+
+const STATS_STORE_INTERVAL = 60 * 1000;
+
+const PREFS_STORE_INTERVAL = 10 * 1000;
+
+const BASE_TIME = Date.now();
+
+
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+Components.utils.import('resource://requestpolicy/DomainUtil.jsm');
+Components.utils.import("resource://requestpolicy/FileUtil.jsm");
+Components.utils.import('resource://requestpolicy/Logger.jsm');
+Components.utils.import('resource://requestpolicy/Telemetry.jsm');
+
+var rp = Components.classes["@requestpolicy.com/requestpolicy-service;1"]
+  .getService(Components.interfaces.nsIRequestPolicy).wrappedJSObject;
+
+var unicodeConverter =
+  Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
+    createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+
+var cryptoHash = Components.classes["@mozilla.org/security/hash;1"]
+  .createInstance(Components.interfaces.nsICryptoHash);
+
+
+var data = {
+  // * Top-level document actions
+  'docs_next_id' : 1,
+  'docs_domain_ids' : {},
+
+  'doc_action_queue': [],
+
+  // destdomain: each key is a destination domain name or IP address whose value
+  // is a dict whose keys are origin domain names or IP addresses. The values of
+  // those are always boolean true.
+  'destdomains' : {},
+  'origindomains' : {},
+
+  // Keep track of mixed content we've already reported so that we can ignore
+  // duplicates.
+  'reported_mixed_content': {},
+
+  // Keep track of non-standar ports we've already reported so that we can
+  // ignore duplicates.
+  'reported_nonstandard_ports': {}
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// General / sendEnvironment / Startup / Shutdown
+///////////////////////////////////////////////////////////////////////////////
+
+function sendEnvironment() {
+  try {
+    var env = {};
+    env['os'] = Components.classes["@mozilla.org/xre/app-info;1"]
+      .getService(Components.interfaces.nsIXULRuntime).OS;
+    var app = Components.classes["@mozilla.org/xre/app-info;1"]
+      .getService(Components.interfaces.nsIXULAppInfo);
+    env['app'] = [app.name, app.version];
+    Telemetry.track('env', env);
+  } catch (e) {
+    Logger.dump('Error in Stats::sendEnvironment: ' + e);
+  }
+}
+
+TRACK_ADDON_IDS = [];
+TRACK_ADDON_IDS.push("requestpolicy at requestpolicy.com"); // RequestPolicy
+// Addons which we have compatibility rules for or which have known conflicts.
+TRACK_ADDON_IDS.push("greasefire at skrul.com"); // GreaseFire
+TRACK_ADDON_IDS.push("{0f9daf7e-2ee2-4fcf-9d4f-d43d93963420}"); // Sage-Too
+TRACK_ADDON_IDS.push("{899DF1F8-2F43-4394-8315-37F6744E6319}"); // NewsFox
+TRACK_ADDON_IDS.push("brief at mozdev.org"); // Brief
+TRACK_ADDON_IDS.push("foxmarks at kei.com"); // Xmarks Sync (a.k.a. Foxmarks)
+TRACK_ADDON_IDS.push("{203FB6B2-2E1E-4474-863B-4C483ECCE78E}"); // Norton Safe Web Lite Toolbar
+TRACK_ADDON_IDS.push("{0C55C096-0F1D-4F28-AAA2-85EF591126E7}"); // Norton Toolbar (a.k.a. NIS Toolbar)
+TRACK_ADDON_IDS.push("{2D3F3651-74B9-4795-BDEC-6DA2F431CB62}"); // Norton Toolbar 2011.7.0.8
+TRACK_ADDON_IDS.push("{c45c406e-ab73-11d8-be73-000a95be3b12}"); // Web Developer
+TRACK_ADDON_IDS.push("{c07d1a49-9894-49ff-a594-38960ede8fb9}"); // Update Scanner
+TRACK_ADDON_IDS.push("FirefoxAddon at similarWeb.com"); // SimilarWeb
+TRACK_ADDON_IDS.push("{6614d11d-d21d-b211-ae23-815234e1ebb5}"); // Dr. Web Link Checker
+// Related addons or addons with potential conflicts.
+TRACK_ADDON_IDS.push("{d40f5e7b-d2cf-4856-b441-cc613eeffbe3}"); // Better Privacy
+TRACK_ADDON_IDS.push("https-everywhere at eff.org"); // HTTPS Everywhere
+TRACK_ADDON_IDS.push("{73a6fe31-595d-460b-a920-fcc0f8843232}"); // NoScript
+TRACK_ADDON_IDS.push("firefox at ghostery.com"); // Ghostery
+TRACK_ADDON_IDS.push("{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}"); // ABP
+TRACK_ADDON_IDS.push("jid1-F9UJ2thwoAm5gQ at jetpack"); // Collusion
+
+function sendAddonsAndPlugins() {
+  try {
+    AddonManager.getAllAddons(function(aAddons) {
+      try {
+        var addons = [];
+        for (var i in aAddons) {
+          var addon = aAddons[i];
+          if (TRACK_ADDON_IDS.indexOf(addon.id) == -1) {
+            continue;
+          }
+          var item = {
+            'id': addon.id,
+            'n': addon.name,
+            'v': addon.version,
+            //'t': addon.type,
+            'a': addon.isActive
+          };
+          if (addon.installDate) {
+            item['i'] = addon.installDate.getTime();
+          }
+          addons.push(item)
+        }
+        Telemetry.track('addons', addons);
+      } catch (e) {
+        Logger.dump('Error in Stats::sendAddonsAndPlugins getAllAddons: ' + e);
+      }
+    });
+  } catch (e) {
+    Logger.dump('Error in Stats::sendAddonsAndPlugins: ' + e);
+  }
+}
+
+function _getPersistentRuleCounts() {
+  var counts = {'o': 0, 'o2d': 0, 'd': 0};
+  for (var i in rp._allowedOrigins) {
+    counts['o']++;
+  }
+  for (var i in rp._allowedOriginsToDestinations) {
+    counts['o2d']++;
+  }
+  for (var i in rp._allowedDestinations) {
+    counts['d']++;
+  }
+  return counts;
+}
+
+
+function _getTempRuleCounts() {
+  var counts = {'o': 0, 'o2d': 0, 'd': 0};
+  for (var i in rp._temporarilyAllowedOrigins) {
+    counts['o']++;
+  }
+  for (var i in rp._temporarilyAllowedOriginsToDestinations) {
+    counts['o2d']++;
+  }
+  for (var i in rp._temporarilyAllowedDestinations) {
+    counts['d']++;
+  }
+  return counts;
+}
+
+
+function sendRuleCount() {
+  try {
+    Telemetry.track('rulecount', {
+      'p': _getPersistentRuleCounts(),
+      't': _getTempRuleCounts()
+    });
+  } catch (e) {
+    Logger.dump('Error in Stats::sendRuleCount: ' + e);
+  }
+}
+
+function sendRPPreferences() {
+  try {
+    var rpPrefs = {
+      'uriIdentificationLevel': rp._uriIdentificationLevel,
+      'log': rp.prefs.getBoolPref('log'),
+      'contextMenu': rp.prefs.getBoolPref('contextMenu'),
+      'autoReload': rp.prefs.getBoolPref('autoReload'),
+      'indicateBlockedObjects': rp.prefs.getBoolPref('indicateBlockedObjects'),
+      'startWithAllowAllEnabled': rp.prefs.getBoolPref('startWithAllowAllEnabled'),
+      'prefetch_link_disableOnStartup': rp.prefs.getBoolPref('prefetch.link.disableOnStartup'),
+      'prefetch_dns_disableOnStartup': rp.prefs.getBoolPref('prefetch.dns.disableOnStartup'),
+      'privateBrowsingPermanentWhitelisting': rp.prefs.getBoolPref('privateBrowsingPermanentWhitelisting')
+    };
+    Telemetry.track('rp_prefs', rpPrefs);
+  } catch (e) {
+    Logger.dump('Error in Stats::sendRPPreferences: ' + e);
+  }
+}
+
+function sendBrowserPreferences() {
+  try {
+    var browserPrefs = {
+      'general_useragent_locale': rp._rootPrefs.getCharPref('general.useragent.locale'),
+      'privacy_donottrackheader_enabled': rp._rootPrefs.getBoolPref('privacy.donottrackheader.enabled'),
+      'places_history_enabled': rp._rootPrefs.getBoolPref('places.history.enabled'),
+      'browser_privatebrowsing_autostart': rp._rootPrefs.getBoolPref('browser.privatebrowsing.autostart'),
+      'network_cookie_cookieBehavior': rp._rootPrefs.getIntPref('network.cookie.cookieBehavior'),
+      'network_cookie_lifetimePolicy': rp._rootPrefs.getIntPref('network.cookie.lifetimePolicy'),
+      'privacy_sanitize_sanitizeOnShutdown': rp._rootPrefs.getBoolPref('privacy.sanitize.sanitizeOnShutdown'),
+      'privacy_sanitize_timeSpan': rp._rootPrefs.getIntPref('privacy.sanitize.timeSpan'),
+      'privacy_clearOnShutdown_cookies': rp._rootPrefs.getBoolPref('privacy.clearOnShutdown.cookies'),
+      'privacy_clearOnShutdown_history': rp._rootPrefs.getBoolPref('privacy.clearOnShutdown.history')
+    };
+    Telemetry.track('browser_prefs', browserPrefs);
+  } catch (e) {
+    Logger.dump('Error in Stats::sendBrowserPreferences: ' + e);
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Preferences
+///////////////////////////////////////////////////////////////////////////////
+
+function sendPrefChanged(pref) {
+  try {
+    var value;
+    if (['log', 'contextMenu', 'autoReload', 'indicateBlockedObjects',
+         'startWithAllowAllEnabled', 'prefetch.link.disableOnStartup',
+         'prefetch.dns.disableOnStartup',
+         'privateBrowsingPermanentWhitelisting'].indexOf(pref) != -1) {
+      value = rp.prefs.getBoolPref(pref);
+    } else if (pref == 'uriIdentificationLevel') {
+      value = rp.prefs.getIntPref(pref);
+    } else {
+      return;
+    }
+    Telemetry.track('prefchanged', {'p': pref, 'v': value});
+    sendRPPreferences();
+  } catch (e) {
+    Logger.dump('Error in Stats::sendPrefChanged: ' + e);
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Top-level document actions
+///////////////////////////////////////////////////////////////////////////////
+
+const DOC_REQUEST = 1;
+const DOC_MENU_OPENED = 2;
+const DOC_MENU_CLOSED = 3;
+const DOC_MENU_ACTION_TEMP_ALLOW_ALL_ENABLED = 4;
+const DOC_MENU_ACTION_TEMP_ALLOW_ALL_DISABLED = 5;
+const DOC_MENU_ACTION_TEMP_ALLOW_ORIGIN = 6;
+const DOC_MENU_ACTION_TEMP_ALLOW_DEST = 7;
+const DOC_MENU_ACTION_TEMP_ALLOW_ORIGIN_TO_DEST = 8;
+const DOC_MENU_ACTION_ALLOW_ORIGIN = 9;
+const DOC_MENU_ACTION_ALLOW_DEST = 10;
+const DOC_MENU_ACTION_ALLOW_ORIGIN_TO_DEST = 11;
+const DOC_MENU_ACTION_FORBID_ORIGIN = 12;
+const DOC_MENU_ACTION_FORBID_DEST = 13;
+const DOC_MENU_ACTION_FORBID_ORIGIN_TO_DEST = 14;
+const DOC_MENU_ACTION_PREFS_OPENED = 15;
+const DOC_MENU_ACTION_REQUEST_LOG_OPENED = 16;
+const DOC_MENU_ACTION_REQUEST_LOG_CLOSED = 17;
+const DOC_MENU_ACTION_REVOKE_TEMP_PERMISSIONS = 18;
+
+
+// Note: this function is also used for the 'mixed' and 'port' events.
+function _getDocsDomainId(uri) {
+  try {
+    var domain = DomainUtil.getDomain(uri);
+  } catch (e) {
+    return -1;
+  }
+  if (!data['docs_domain_ids'][domain]) {
+    data['docs_domain_ids'][domain] = data['docs_next_id']++;
+  }
+  return data['docs_domain_ids'][domain];
+}
+
+
+function recordDocAction(uri, type, extra) {
+  try {
+    var action = {d: _getDocsDomainId(uri), t: type};
+    if (extra) {
+      action.e = extra;
+    }
+    action.rt = Date.now() - BASE_TIME;
+    data['doc_action_queue'].push(action);
+  } catch (e) {
+    Logger.dump('Stats::recordDocAction error: ' + e);
+  }
+}
+
+
+function sendDocs() {
+  try {
+    var props = data['doc_action_queue'];
+    data['doc_action_queue'] = [];
+    if (props.length == 0) {
+      return;
+    }
+    Telemetry.track('docs', props);
+  } catch (e) {
+    Logger.dump('Stats::sendDocs error: ' + e);
+  }
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Preferences window actions
+///////////////////////////////////////////////////////////////////////////////
+
+const PREF_WINDOW_LOADED = 1;
+const PREF_WINDOW_ALLOW_ORIGIN = 2;
+const PREF_WINDOW_ALLOW_DEST = 3;
+const PREF_WINDOW_ALLOW_ORIGIN_TO_DEST = 4;
+const PREF_WINDOW_FORBID_ORIGIN = 5;
+const PREF_WINDOW_FORBID_DEST = 6;
+const PREF_WINDOW_FORBID_ORIGIN_TO_DEST = 7;
+const PREF_WINDOW_IMPORT = 8;
+const PREF_WINDOW_EXPORT = 9;
+const PREF_WINDOW_CLOSED = 10;
+
+function sendPrefWindow(type, extra) {
+  try {
+    var props = {t: type};
+    if (extra) {
+      props.e = extra;
+    }
+    Telemetry.track('prefwin', props);
+  } catch (e) {
+    Logger.dump('Stats::sendPrefWindow error: ' + e);
+  }
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Requests - general
+///////////////////////////////////////////////////////////////////////////////
+
+/*
+ * Data we use to figure out how many origins per dest and how many dests per
+ * origin.
+ */
+function recordEdges(origin, dest) {
+  var originDomain = DomainUtil.getDomain(origin);
+  var destDomain = DomainUtil.getDomain(dest);
+  if (originDomain == destDomain) {
+    return;
+  }
+
+  if (!data['destdomains'][destDomain]) {
+    data['destdomains'][destDomain] = {};
+  }
+  if (!data['destdomains'][destDomain][originDomain]) {
+    data['destdomains'][destDomain][originDomain] = true;
+  }
+
+  if (!data['origindomains'][originDomain]) {
+    data['origindomains'][originDomain] = {};
+  }
+  if (!data['origindomains'][originDomain][destDomain]) {
+    data['origindomains'][originDomain][destDomain] = true;
+  }
+
+}
+
+NSICONTENTPOLICY_TYPE_DOCUMENT = 6;
+
+/*
+ * Data on http requests from https documents.
+ */
+function sendInsecureMixedContent(origin, dest, contentType) {
+  var originScheme = DomainUtil.getUriObject(origin).scheme;
+  var destScheme = DomainUtil.getUriObject(dest).scheme;
+
+  if (originScheme == 'https' && destScheme == 'http' &&
+      contentType != NSICONTENTPOLICY_TYPE_DOCUMENT) {
+
+    var mixed = {
+      t: contentType,
+      o: _getDocsDomainId(origin),
+      d: _getDocsDomainId(dest)
+    };
+    // Ignore duplicates.
+    var reported = data['reported_mixed_content'];
+    if (reported[mixed.o] &&
+        reported[mixed.o][mixed.d] &&
+        reported[mixed.o][mixed.d][mixed.t]) {
+      return;
+    }
+    reported[mixed.o] = reported[mixed.o] || {};
+    reported[mixed.o][mixed.d] = reported[mixed.o][mixed.d] || {};
+    reported[mixed.o][mixed.d][mixed.t] = reported[mixed.o][mixed.d][mixed.t] || {};
+
+    Telemetry.track('mixed', mixed);
+  }
+}
+
+
+/*
+ * Data about requests to non-standard ports.
+ */
+function sendNonStandardPorts(origin, dest, contentType) {
+  var originUriObj = DomainUtil.getUriObject(origin);
+  var destUriObj = DomainUtil.getUriObject(dest);
+  var originPort = originUriObj.port;
+  var destPort = destUriObj.port;
+
+  // Default ports for a given protocol will be -1.
+  if (originPort == -1 && destPort == -1) {
+    return;
+  }
+
+  // If they're the same non-standard port, only report it if it's a top-level
+  // document load. We don't want every single request from a page on a
+  // non-standard port to trigger an event.
+  if (originPort == destPort && contentType != NSICONTENTPOLICY_TYPE_DOCUMENT) {
+    return;
+  }
+
+  var props = {
+    t: contentType,
+    o: _getDocsDomainId(origin),
+    d: _getDocsDomainId(dest),
+    o_s: originUriObj.scheme,
+    d_s: destUriObj.scheme,
+    o_nsp: originPort != -1,
+    d_nsp: destPort != -1
+  };
+
+  // Ignore duplicates.
+  var key = props.o + '-' + props.d + '-' + props.t + '-' + props.o_s + '-' +
+      props.d_s + '-' + props.o_nsp + '-' + props.d_nsp;
+  if (data['reported_nonstandard_ports'][key]) {
+    return;
+  }
+  data['reported_nonstandard_ports'][key] = true;
+
+  Telemetry.track('port', props);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+// A comparison function for sort().
+function numeric(a, b) {
+  return a - b;
+}
+
+function sendRules() {
+  try {
+    Telemetry.track('rules', ruleData.rules);
+  } catch (e) {
+    Logger.dump('Stats.sendRules error: ' + e);
+  }
+}
+
+
+function sendTempRules() {
+  try {
+    Telemetry.track('temprules', ruleData.temprules);
+  } catch (e) {
+    Logger.dump('Stats.sendTempRules error: ' + e);
+  }
+}
+
+
+function sendEdges() {
+  try {
+    var edges = {'o-per-d': [], 'd-per-o': []};
+
+    for (var dest in data['destdomains']) {
+      var count = 0;
+      for (var origin in data['destdomains'][dest]) {
+        count++;
+      }
+      edges['o-per-d'].push(count);
+    }
+
+    for (var origin in data['origindomains']) {
+      //Logger.dump('origindomains origin: ' + origin);
+      var count = 0;
+      for (var dest in data['origindomains'][origin]) {
+        count++;
+      }
+      edges['d-per-o'].push(count);
+    }
+
+    edges['o-per-d'].sort(numeric);
+    edges['d-per-o'].sort(numeric);
+    Telemetry.track('edges', edges);
+  } catch (e) {
+    Logger.dump('Stats.edges error: ' + e);
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+// args = [aContentType, dest, origin, aContext, aMimeTypeGuess, aExtra]
+
+function handleStatsShouldLoadCall(type, args) {
+  try {
+    var contentType = args[0];
+    var dest = args[1];
+    var origin = args[2];
+
+    recordEdges(origin, dest);
+    sendInsecureMixedContent(origin, dest, contentType);
+    sendNonStandardPorts(origin, dest, contentType);
+  } catch (e) {
+    Logger.dump('Stats::handleStatsShouldLoadCall error: ' + e);
+  }
+}
+
+/**
+ * Statistics gathering.
+ */
+var Stats = {
+
+  reportStatsHighFrequency : function() {
+    sendTempRules();
+    sendEdges();
+  },
+
+  reportStatsMedFrequency : function() {
+    sendRules();
+  },
+
+  // Reminder: Low frequency stats aren't sent on browser shutdown.
+  reportStatsLowFrequency : function() {
+    sendBrowserPreferences();
+    sendAddonsAndPlugins();
+  },
+
+  reportQueuedDocActions : function() {
+    sendDocs();
+  },
+
+  browserStarted : function() {
+    Telemetry.track('browser_started');
+    ruleData.loadFromFile();
+    ruleData.importExistingRules();
+    this.reportStatsLowFrequency();
+    sendEnvironment();
+    sendRuleCount();
+    sendRPPreferences();
+  },
+
+  consentGranted : function() {
+    Telemetry.track('consent_granted');
+    ruleData.reset();
+    ruleData.importExistingRules();
+    this.reportStatsHighFrequency();
+    this.reportStatsMedFrequency();
+    this.reportStatsLowFrequency();
+    sendEnvironment();
+    sendRuleCount();
+    sendRPPreferences();
+  },
+
+  consentRevoked : function() {
+    this.reportQueuedDocActions();
+    Telemetry.track('consent_revoked');
+  },
+
+  prefChanged : function(pref) {
+    sendPrefChanged(pref);
+  },
+
+  requestedTopLevelDocument : function(uri) {
+    recordDocAction(uri, DOC_REQUEST);
+  },
+
+  menuOpened : function(uri, blockedDestCount, allowedDestCount, otherOriginCount) {
+    recordDocAction(
+      uri, DOC_MENU_OPENED,
+      {'bd': blockedDestCount, 'ad': allowedDestCount, 'oo': otherOriginCount});
+  },
+
+  menuClosed : function(uri) {
+    recordDocAction(uri, DOC_MENU_CLOSED);
+  },
+
+  menuActionTempAllowAllEnabled : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_TEMP_ALLOW_ALL_ENABLED);
+  },
+
+  menuActionTempAllowAllDisabled : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_TEMP_ALLOW_ALL_DISABLED);
+  },
+
+  menuActionAllowOrigin : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_ALLOW_ORIGIN);
+  },
+
+  menuActionAllowDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_ALLOW_DEST);
+  },
+
+  menuActionAllowOriginToDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_ALLOW_ORIGIN_TO_DEST);
+  },
+
+  menuActionTempAllowOrigin : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_TEMP_ALLOW_ORIGIN);
+  },
+
+  menuActionTempAllowDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_TEMP_ALLOW_DEST);
+  },
+
+  menuActionTempAllowOriginToDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_TEMP_ALLOW_ORIGIN_TO_DEST);
+  },
+
+  menuActionForbidOrigin : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_FORBID_ORIGIN);
+  },
+
+  menuActionForbidDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_FORBID_DEST);
+  },
+
+  menuActionForbidOriginToDest : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_FORBID_ORIGIN_TO_DEST);
+  },
+
+  menuActionPrefsOpened : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_PREFS_OPENED);
+  },
+
+  menuActionRequestLogOpened : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_REQUEST_LOG_OPENED);
+  },
+
+  menuActionRequestLogClosed : function(uri) {
+    recordDocAction(uri, DOC_MENU_ACTION_REQUEST_LOG_CLOSED);
+  },
+
+  menuActionRevokeTempPerms : function(uri) {
+    sendRuleCount();
+    recordDocAction(uri, DOC_MENU_ACTION_REVOKE_TEMP_PERMISSIONS);
+  },
+
+  prefWindowLoaded : function() {
+    sendPrefWindow(PREF_WINDOW_LOADED);
+  },
+
+  prefWindowClosed : function(extra) {
+    sendPrefWindow(PREF_WINDOW_CLOSED, extra);
+  },
+
+  prefWindowAllowOrigin : function() {
+    sendPrefWindow(PREF_WINDOW_ALLOW_ORIGIN);
+  },
+
+  prefWindowAllowDest : function() {
+    sendPrefWindow(PREF_WINDOW_ALLOW_DEST);
+  },
+
+  prefWindowAllowOriginToDest : function() {
+    sendPrefWindow(PREF_WINDOW_ALLOW_ORIGIN_TO_DEST);
+  },
+
+  prefWindowForbidOrigin : function() {
+    sendPrefWindow(PREF_WINDOW_FORBID_ORIGIN);
+  },
+
+  prefWindowForbidDest : function() {
+    sendPrefWindow(PREF_WINDOW_FORBID_DEST);
+  },
+
+  prefWindowForbidOriginToDest : function() {
+    sendPrefWindow(PREF_WINDOW_FORBID_ORIGIN_TO_DEST);
+  },
+
+  prefWindowImport : function() {
+    sendPrefWindow(PREF_WINDOW_IMPORT);
+  },
+
+  prefWindowExport : function() {
+    sendPrefWindow(PREF_WINDOW_EXPORT);
+  },
+
+  allowedSameHost : function(args) {
+    handleStatsShouldLoadCall(TYPE_ALLOWED_SAME_HOST, args);
+  },
+
+  // For now, we don't have any link click or form submission data we're
+  // tracking. We might want to track how many cross-site form submissions there
+  // are.
+  allowedLinkClick : function(args) {
+    //handleStatsShouldLoadCall(TYPE_ALLOWED_LINK_CLICK, args);
+  },
+  allowedFormSubmission : function(args) {
+    //handleStatsShouldLoadCall(TYPE_ALLOWED_FORM_SUBMISSION, args);
+  },
+
+  _ruleMatched : function(args, originIdent, destIdent, temp) {
+    try {
+      ruleData.ruleMatched(args, originIdent, destIdent, temp);
+    } catch (e) {
+      Logger.dump('error in _ruleMatched: ' + e);
+    }
+  },
+
+  allowedByOriginToDest : function(args, originIdent, destIdent) {
+    this._ruleMatched(args, originIdent, destIdent);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_ORIGIN_TO_DEST, args);
+  },
+
+  allowedByOrigin : function(args, originIdent) {
+    this._ruleMatched(args, originIdent, null);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_ORIGIN, args);
+  },
+
+  allowedByDest : function(args, destIdent) {
+    this._ruleMatched(args, null, destIdent);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_DEST, args);
+  },
+
+  tempAllowedByOriginToDest : function(args, originIdent, destIdent) {
+    this._ruleMatched(args, originIdent, destIdent, true);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_TEMP_ORIGIN_TO_DEST, args);
+  },
+
+  tempAllowedByOrigin : function(args, originIdent) {
+    this._ruleMatched(args, originIdent, null, true);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_TEMP_ORIGIN, args);
+  },
+
+  tempAllowedByDest : function(args, destIdent) {
+    this._ruleMatched(args, null, destIdent, true);
+    handleStatsShouldLoadCall(TYPE_ALLOWED_RULE_TEMP_DEST, args);
+  },
+
+  denied : function(args) {
+    handleStatsShouldLoadCall(TYPE_DENIED, args);
+  },
+
+  // Rule creation/deletion
+
+  _ruleAdded: function(originIdent, destIdent, temp) {
+    try {
+      sendRuleCount();
+      ruleData.ruleAdded(originIdent, destIdent, temp);
+    } catch (e) {
+      Logger.dump('error in _ruleAdded: ' + e);
+    }
+  },
+
+  ruleAddedOrigin: function(originIdent) {
+    this._ruleAdded(originIdent, null);
+  },
+
+  ruleAddedDest: function(destIdent) {
+    this._ruleAdded(null, destIdent);
+  },
+
+  ruleAddedOriginToDest: function(originIdent, destIdent) {
+    this._ruleAdded(originIdent, destIdent);
+  },
+
+  ruleAddedTempOrigin: function(originIdent) {
+    this._ruleAdded(originIdent, null, true);
+  },
+
+  ruleAddedTempDest: function(destIdent) {
+    this._ruleAdded(null, destIdent, true);
+  },
+
+  ruleAddedTempOriginToDest: function(originIdent, destIdent) {
+    this._ruleAdded(originIdent, destIdent, true);
+  },
+
+  _ruleRemoved: function(originIdent, destIdent, temp) {
+    try {
+      sendRuleCount();
+      ruleData.ruleRemoved(originIdent, destIdent, temp);
+    } catch (e) {
+      Logger.dump('error in _ruleRemoved: ' + e);
+    }
+  },
+
+  ruleRemovedOrigin: function(originIdent) {
+    this._ruleRemoved(originIdent, null);
+  },
+
+  ruleRemovedDest: function(destIdent) {
+    this._ruleRemoved(null, destIdent);
+  },
+
+  ruleRemovedOriginToDest: function(originIdent, destIdent) {
+    this._ruleRemoved(originIdent, destIdent);
+  },
+
+  ruleRemovedTempOrigin: function(originIdent) {
+    this._ruleRemoved(originIdent, null, true);
+  },
+
+  ruleRemovedTempDest: function(destIdent) {
+    this._ruleRemoved(null, destIdent, true);
+  },
+
+  ruleRemovedTempOriginToDest: function(originIdent, destIdent) {
+    this._ruleRemoved(originIdent, destIdent, true);
+  },
+
+  deleteFile : function() {
+    ruleData.deleteFile();
+  },
+
+  saveFile : function() {
+    ruleData.saveToFile();
+  }
+};
+
+
+function hourTimestamp() {
+  // Timestamp in seconds rounded down to the nearest hour.
+  return Math.floor(Date.now() / 1000 / 3600) * 3600;
+}
+
+// return the two-digit hexadecimal code for a byte
+function toHexString(charCode) {
+  return ('0' + charCode.toString(16)).slice(-2);
+}
+
+function getHash(str) {
+  unicodeConverter.charset = 'UTF-8';
+  var result = {};
+  var data = unicodeConverter.convertToByteArray(str, result);
+  cryptoHash.init(cryptoHash.MD5);
+  cryptoHash.update(data, data.length);
+  var hash = cryptoHash.finish(false);
+  // We don't need 128 bits. Let's take 32. Very roughly, that should give us
+  // a 0.01% chance of two strings out of a thousand having the same key.
+  hash = hash.slice(-4);
+  return [toHexString(hash.charCodeAt(i)) for (i in hash)].join('');
+}
+
+function isValidIdent(ident) {
+  if (!DomainUtil.isValidUri(ident)) {
+    ident = 'http://' + ident;
+  }
+  // If it's still not valid as a uri, it can't be a valid ident.
+  if (!DomainUtil.isValidUri(ident)) {
+    return false;
+  }
+  // If it has a path or a scheme like "http:/" (so we had prepended one above),
+  // it's not valid.
+  if (DomainUtil.getUriObject(ident).prePath != ident) {
+    return false;
+  }
+  return true;
+}
+
+function generateSalt() {
+  const length = 32; // bytes
+  var buffer = '';
+  var prng = Components.classes['@mozilla.org/security/random-generator;1'];
+  var bytes =  prng.getService(Components.interfaces.nsIRandomGenerator)
+      .generateRandomBytes(length, buffer);
+  var salt = [toHexString(bytes[i]) for (i in bytes)].join('');
+  return salt;
+}
+
+
+var ruleData = {
+
+  salt: null,
+  keys: [],
+  tempkeys: [], // not stored
+  rules: {},
+  temprules: {}, // not stored
+
+  reset: function() {
+    this.salt = generateSalt();
+    this.keys = [];
+    this.rules = {};
+    this.tempkeys = [];
+    this.temprules = {};
+  },
+
+  initialize : function(data) {
+    if (data.salt && data.keys && data.rules) {
+      this.salt = data.salt;
+      this.keys = data.keys;
+      this.rules = data.rules;
+    } else {
+      this.salt = generateSalt();
+    }
+  },
+
+  _getKeyID : function(string, temp) {
+    // Sanity check: it would be bad if a bug caused the salt to not be set.
+    if (!this.salt) {
+      throw 'Invalid salt: ' + this.salt;
+    }
+    var hash = getHash(this.salt + string);
+    var keys = temp ? this.tempkeys : this.keys;
+    if (keys.indexOf(hash) == -1) {
+      keys.push(hash);
+    }
+    return hash;
+  },
+
+  importExistingRules: function() {
+    try {
+      for (var originIdent in rp._allowedOrigins) {
+        this._getRuleRecord(originIdent, null);
+      }
+      for (var destIdent in rp._allowedDestinations) {
+        this._getRuleRecord(null, destIdent);
+      }
+      for (var combinedIdent in rp._allowedOriginsToDestinations) {
+        var parts = combinedIdent.split('|');
+        var originIdent = parts[0];
+        var destIdent = parts[1];
+        this._getRuleRecord(originIdent, destIdent);
+      }
+
+      for (var originIdent in rp._temporarilyAllowedOrigins) {
+        this._getRuleRecord(originIdent, null, true);
+      }
+      for (var destIdent in rp._temporarilyAllowedDestinations) {
+        this._getRuleRecord(null, destIdent, true);
+      }
+      for (var combinedIdent in rp._temporarilyAllowedOriginsToDestinations) {
+        var parts = combinedIdent.split('|');
+        var originIdent = parts[0];
+        var destIdent = parts[1];
+        this._getRuleRecord(originIdent, destIdent, true);
+      }
+    } catch (e) {
+      Logger.dump('error importing existing rules into stats data: ' + e);
+    }
+  },
+
+  _generateRuleRecord : function(originIdent, destIdent, temp) {
+    var record = {};
+    if (isSuggestedRule(originIdent, destIdent)) {
+        record.sg = true;
+    }
+
+    if (originIdent) {
+      record.o = {};
+      if (!isValidIdent(originIdent)) {
+        record.o.inv = true;
+      } else {
+        if (DomainUtil.isValidUri(originIdent)) {
+          var useOrigin = originIdent;
+          record.o.s = true; // includes scheme
+          // Non-standard port
+          record.o.nsp = DomainUtil.getUriObject(useOrigin).port != -1;
+        } else {
+          useOrigin = 'http://' + originIdent;
+          record.o.s = false; // does not include scheme
+        }
+        if (DomainUtil.getHost(useOrigin) == DomainUtil.getDomain(useOrigin)) {
+          if (DomainUtil.isIPAddress(DomainUtil.getUriObject(useOrigin).host)) {
+            record.o.ht = 'a'; // address
+          } else {
+            record.o.ht = 'd'; // registered domain or non-dotted hostname
+          }
+        } else {
+          record.o.ht = 's'; // subdomain
+        }
+
+        var originIdentKey = this._getKeyID(originIdent);
+        var originHostKey = this._getKeyID(DomainUtil.getHost(useOrigin), temp);
+        var originDomainKey = this._getKeyID(DomainUtil.getDomain(useOrigin), temp);
+        if (record.o.ht != 'a' && originDomainKey != originIdentKey) {
+          record.o.dk = originDomainKey;
+        }
+        if (record.o.ht == 'a' ||
+            (originHostKey != originIdentKey && originHostKey != originDomainKey)) {
+          record.o.hk = originHostKey;
+        }
+      }
+    }
+
+    if (destIdent) {
+      record.d = {};
+      if (!isValidIdent(destIdent)) {
+        record.d.inv = true;
+      } else {
+        if (DomainUtil.isValidUri(destIdent)) {
+          var useDest = destIdent;
+          record.d.s = true; // includes scheme
+          // Non-standard port
+          record.d.nsp = DomainUtil.getUriObject(useDest).port != -1;
+        } else {
+          useDest = 'http://' + destIdent;
+          record.d.s = false; // does not include scheme
+        }
+        if (DomainUtil.getHost(useDest) == DomainUtil.getDomain(useDest)) {
+          if (DomainUtil.isIPAddress(DomainUtil.getUriObject(useDest).host)) {
+            record.d.ht = 'a'; // address
+          } else {
+            record.d.ht = 'd'; // registered domain or non-dotted hostname
+          }
+        } else {
+          record.d.ht = 's'; // subdomain
+        }
+
+        var destIdentKey = this._getKeyID(destIdent);
+        var destHostKey = this._getKeyID(DomainUtil.getHost(useDest), temp);
+        var destDomainKey = this._getKeyID(DomainUtil.getDomain(useDest), temp);
+        if (record.d.ht != 'a' && destDomainKey != destIdentKey) {
+          record.d.dk = destDomainKey;
+        }
+        if (record.d.ht == 'a' ||
+            (destHostKey != destIdentKey && destHostKey != destDomainKey)) {
+          record.d.hk = destHostKey;
+        }
+      }
+    }
+
+    return record;
+  },
+
+  _getRuleRecord : function(originIdent, destIdent, temp) {
+    var rules = temp ? this.temprules : this.rules;
+
+    var originKey = originIdent ? this._getKeyID(originIdent, temp) :  '';
+    var destKey = destIdent ? this._getKeyID(destIdent, temp) : '';
+    var ruleID = originKey + '|' + destKey;
+
+    if (!rules[ruleID]) {
+      rules[ruleID] = this._generateRuleRecord(originIdent, destIdent, temp);
+    }
+    return rules[ruleID];
+  },
+
+  ruleAdded: function(originIdent, destIdent, temp) {
+    var record = this._getRuleRecord(originIdent, destIdent, temp);
+    if (!record['h']) {
+        record['h'] = [];
+    }
+    record['h'].push(['c', hourTimestamp()]);
+  },
+
+  ruleRemoved: function(originIdent, destIdent, temp) {
+    var record = this._getRuleRecord(originIdent, destIdent, temp);
+    if (!record['h']) {
+        record['h'] = [];
+    }
+    record['h'].push(['d', hourTimestamp()]);
+  },
+
+  ruleMatched: function(args, originIdent, destIdent, temp) {
+    var record = this._getRuleRecord(originIdent, destIdent, temp);
+    record['l'] = hourTimestamp();
+    if (!record.t) {
+      record.t = [];
+    }
+    if (record.t.indexOf(args[0]) == -1) {
+      record.t.push(args[0]);
+    }
+  },
+
+  loadFromFile : function() {
+    try {
+      var file = FileUtil.getRPUserDir();
+      file.appendRelativePath(STORED_STATS_FILENAME);
+      var str = FileUtil.fileToString(file);
+      if (!str) {
+        throw 'stats file is empty';
+      }
+      var data = JSON.parse(str);
+    } catch (e) {
+      Logger.dump('Unable to load stored stats.');
+      data = {};
+    }
+    this.initialize(data);
+  },
+
+  saveToFile : function() {
+    if (!Telemetry.getEnabled() || Telemetry.isPastEndDate()) {
+      return;
+    }
+    var data = {
+      salt: this.salt,
+      keys: this.keys,
+      rules: this.rules
+    };
+
+    try {
+      var file = FileUtil.getRPUserDir();
+      file.appendRelativePath(STORED_STATS_FILENAME);
+      var str = JSON.stringify(data);
+      FileUtil.stringToFile(str, file);
+    } catch (e) {
+      Logger.dump('Unable to save stored stats: ' + e);
+    }
+  },
+
+  deleteFile : function() {
+    try {
+      var file = FileUtil.getRPUserDir();
+      file.appendRelativePath(STORED_STATS_FILENAME);
+      file.remove(false);
+    } catch (e) {
+      Logger.dump('Unable to delete stored stats: ' + e);
+    }
+  }
+
+};
+
+
+// From initialSetup.js
+var rawSuggestedItems = [
+   ["yahoo.com", "yimg.com"],
+    ["paypal.com", "paypalobjects.com"],
+    ["google.com", "googlehosted.com"], ["google.com", "gvt0.com"],
+    ["google.com", "youtube.com"], ["google.com", "ggpht.com"],
+    ["google.com", "gstatic.com"], ["gmail.com", "google.com"],
+    ["googlemail.com", "google.com"], ["youtube.com", "ytimg.com"],
+    ["youtube.com", "google.com"], ["youtube.com", "googlevideo.com"],
+    ["live.com", "msn.com"], ["msn.com", "live.com"],
+    ["live.com", "virtualearth.net"], ["live.com", "wlxrs.com"],
+    ["hotmail.com", "passport.com"], ["passport.com", "live.com"],
+    ["live.com", "hotmail.com"], ["microsoft.com", "msn.com"],
+    ["microsoft.com", "live.com"], ["live.com", "microsoft.com"],
+    ["facebook.com", "fbcdn.net"], ["myspace.com", "myspacecdn.com"],
+    ["wikipedia.com", "wikipedia.org"], ["wikipedia.org", "wikimedia.org"],
+    ["wiktionary.org", "wikimedia.org"],
+    ["wikibooks.org", "wikimedia.org"],
+    ["wikiversity.org", "wikimedia.org"],
+    ["wikisource.org", "wikimedia.org"], ["wikinews.org", "wikimedia.org"],
+    ["blogger.com", "google.com"], ["google.com", "blogger.com"],
+    ["blogspot.com", "blogger.com"], ["flickr.com", "yimg.com"],
+    ["flickr.com", "yahoo.com"], ["imdb.com", "media-imdb.com"],
+    ["fotolog.com", "fotologs.net"], ["metacafe.com", "mcstatic.com"],
+    ["metacafe.com", "mccont.com"], ["download.com", "com.com"],
+    ["cnet.com", "com.com"], ["gamespot.com", "com.com"],
+    ["sf.net", "sourceforge.net"], ["sourceforge.net", "fsdn.com"],
+    ["mapquest.com", "mqcdn.com"], ["mapquest.com", "aolcdn.com"],
+    ["mapquest.com", "aol.com"], ["twitter.com", "twimg.com"],
+   ["orkut.com", "google.com"], ["orkut.com.br", "google.com"],
+    ["uol.com.br", "imguol.com"], ["google.com", "orkut.com"],
+   ["orkut.com", "google.com"], ["orkut.co.in", "google.com"],
+    ["google.com", "orkut.com"], ["yahoo.co.jp", "yimg.jp"],
+    ["sina.com.cn", "sinaimg.cn"], ["amazon.co.jp", "images-amazon.com"],
+    ["amazon.co.jp", "ssl-images-amazon.com"],
+    ["amazon.cn", "images-amazon.com"],
+    ["amazon.cn", "ssl-images-amazon.com"], ["amazon.cn", "joyo.com"],
+    ["joyo.com", "amazon.cn"], ["taobao.com", "taobaocdn.com"],
+    ["163.com", "netease.com"], ["daum.net", "daum-img.net"],
+    ["tudou.com", "tudouui.com"],
+   ["ebay.ca", "ebaystatic.com"], ["ebay.ca", "ebay.com"],
+    ["ebay.com", "ebay.ca"], ["ebay.com", "ebaystatic.com"],
+    ["amazon.com", "images-amazon.com"],
+    ["amazon.com", "ssl-images-amazon.com"],
+    ["amazon.ca", "images-amazon.com"],
+    ["amazon.ca", "ssl-images-amazon.com"], ["aol.com", "aolcdn.com"],
+    ["cnn.com", "turner.com"], ["cnn.com", "cnn.net"],
+    ["tagged.com", "tagstat.com"], ["comcast.net", "cimcontent.net"],
+    ["weather.com", "imwx.com"], ["netflix.com", "nflximg.com"],
+   ["ebay.de", "ebaystatic.com"], ["ebay.de", "ebay.com"],
+    ["ebay.com", "ebay.de"], ["ebay.co.uk", "ebaystatic.com"],
+    ["ebay.co.uk", "ebay.com"], ["ebay.com", "ebay.co.uk"],
+    ["ebay.fr", "ebaystatic.com"], ["ebay.fr", "ebay.com"],
+    ["ebay.com", "ebay.fr"], ["mail.ru", "imgsmail.ru"],
+    ["amazon.de", "images-amazon.com"],
+    ["amazon.de", "ssl-images-amazon.com"],
+    ["amazon.co.uk", "images-amazon.com"],
+    ["amazon.co.uk", "ssl-images-amazon.com"],
+    ["amazon.fr", "images-amazon.com"],
+    ["amazon.fr", "ssl-images-amazon.com"], ["yandex.ru", "yandex.net"],
+    ["skyrock.com", "skyrock.net"], ["netlog.com", "netlogstatic.com"],
+    ["rambler.ru", "rl0.ru"], ["orange.fr", "woopic.com"],
+   ["ebay.com.au", "ebaystatic.com"],
+    ["ebay.com.au", "ebay.com"], ["ebay.com", "ebay.com.au"]
+];
+
+var suggestedItems = [];
+for (var i in rawSuggestedItems) {
+  suggestedItems.push(rawSuggestedItems[i][0] + '|' + rawSuggestedItems[i][1]);
+}
+
+function isSuggestedRule(originIdent, destIdent) {
+  if (!originIdent) {
+    originIdent = '';
+  }
+  if (!destIdent) {
+    destIdent = '';
+  }
+  return suggestedItems.indexOf(originIdent + '|' + destIdent) != -1;
+}
+
+
+var reportTimerDoc = Components.classes["@mozilla.org/timer;1"].createInstance(
+    Components.interfaces.nsITimer);
+reportTimerDoc.initWithCallback(
+    Stats.reportQueuedDocActions, STATS_REPORT_INTERVAL_DOC,
+    Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+
+var reportTimerHighFrequency = Components.classes["@mozilla.org/timer;1"].createInstance(
+    Components.interfaces.nsITimer);
+reportTimerHighFrequency.initWithCallback(
+    Stats.reportStatsHighFrequency, STATS_REPORT_INTERVAL_HIGH_FREQUENCY,
+    Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+
+var reportTimerMedFrequency = Components.classes["@mozilla.org/timer;1"].createInstance(
+    Components.interfaces.nsITimer);
+reportTimerMedFrequency.initWithCallback(
+    Stats.reportStatsMedFrequency, STATS_REPORT_INTERVAL_MED_FREQUENCY,
+    Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+
+var reportTimerLowFrequency = Components.classes["@mozilla.org/timer;1"].createInstance(
+    Components.interfaces.nsITimer);
+reportTimerLowFrequency.initWithCallback(
+    Stats.reportStatsLowFrequency, STATS_REPORT_INTERVAL_LOW_FREQUENCY,
+    Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+
+var statsSaveTimer = Components.classes["@mozilla.org/timer;1"].createInstance(
+  Components.interfaces.nsITimer);
+statsSaveTimer.initWithCallback(function() {
+  ruleData.saveToFile();
+}, STATS_STORE_INTERVAL, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+
+var prefsSaveTimer = Components.classes["@mozilla.org/timer;1"].createInstance(
+    Components.interfaces.nsITimer);
+prefsSaveTimer.initWithCallback(function() {
+  if (rp.globalEventIDChanged) {
+    rp.globalEventIDChanged = false;
+    rp._prefService.savePrefFile(null);
+  }
+}, PREFS_STORE_INTERVAL, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
diff --git a/src/modules/Telemetry.jsm b/src/modules/Telemetry.jsm
new file mode 100644
index 0000000..771eca2
--- /dev/null
+++ b/src/modules/Telemetry.jsm
@@ -0,0 +1,217 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * RequestPolicy - A Firefox extension for control over cross-site requests.
+ * Copyright (c) 2011 Justin Samuel
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EXPORTED_SYMBOLS = ['Telemetry'];
+
+Components.utils.import('resource://requestpolicy/Logger.jsm');
+
+const TELEMETRY_SEND_INTERVAL = 1 * 60 * 1000;
+
+const TELEMETRY_SEND_URL = 'https://telemetry.requestpolicy.com/api/rp.study.submit/';
+
+// After this date, the study will certainly have been ended. No new events
+// will be generated after this date. No new data will be stored locally after
+// this date. Any locally stored data will be deleted the first time the browser
+// starts after this date.
+const END_DATE = new Date(2013, 1, 1, 0, 0, 0, 0);
+
+var profileID = 0;
+
+var consentID = 0;
+
+var sessionID = 0;
+
+var enabled = false;
+
+var eventID = 0;
+
+var globalEventID = 0;
+
+var queue = [];
+
+var rp = Components.classes["@requestpolicy.com/requestpolicy-service;1"]
+    .getService(Components.interfaces.nsIRequestPolicy).wrappedJSObject;
+
+
+function getRandomID() {
+  var min = 1;
+  var max = 10e16;
+  return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function getTimestamp() {
+  return Date.now();
+}
+
+function getEventID() {
+  return ++eventID;
+}
+
+function getGlobalEventID() {
+  globalEventID++;
+  rp.prefs.setIntPref('study.globalEventID', globalEventID);
+  rp.globalEventIDChanged = true;
+  return globalEventID;
+}
+
+/*
+ * When final is true, synchronous communication will be used because the
+ * browser is shutting down.
+ */
+function sendEvents(events, final) {
+  try {
+    var formData = Components.classes["@mozilla.org/files/formdata;1"]
+      .createInstance(Components.interfaces.nsIDOMFormData);
+    formData.append('events', JSON.stringify(events));
+
+    var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+      .createInstance(Components.interfaces.nsIXMLHttpRequest);
+
+    if (!final) {
+      req.addEventListener('load', function(event) {
+        Logger.dump('Telemetry events sending success');
+      }, false);
+      req.addEventListener('error', function(event) {
+        reenqueue(events);
+        Logger.dump('Telemetry events sending failure: ' + event);
+      }, false);
+    } else {
+      Logger.dump('Telemetry events sending sync');
+    }
+
+    req.open('POST', TELEMETRY_SEND_URL, !final);
+    // When sending sync, send() can raise an exception.
+    req.send(formData);
+
+    if (final) {
+      if (req.status == 200) {
+        Logger.dump('Telemetry events final sending success');
+      } else {
+        Logger.dump('Telemetry events final sending failure: ' + req.status);
+      }
+    }
+  } catch (e) {
+    Logger.dump('Telemetry events sending failure: ' + e);
+  }
+}
+
+function processQueue(synchronous) {
+  if (queue.length == 0) {
+    return;
+  }
+  var events = queue;
+  queue = [];
+  var final = synchronous ? true : false;
+  sendEvents(events, final);
+}
+
+function enqueue(event) {
+  queue.push(event);
+  Logger.dump('Telemetry queue size: ' + queue.length);
+}
+
+function reenqueue(events) {
+  queue = events.concat(queue);
+  Logger.dump('Telemetry queue size: ' + queue.length);
+}
+
+/**
+ * Telemetry: reporting data back to the developers.
+ */
+var Telemetry = {
+
+  isPastEndDate: function() {
+    return new Date() > END_DATE;
+  },
+
+  setEnabled : function(isEnabled) {
+    enabled = isEnabled;
+  },
+
+  getEnabled : function() {
+    return enabled;
+  },
+
+  track : function(name, properties) {
+    try {
+      if (!enabled || this.isPastEndDate()) {
+        return;
+      }
+      if (!profileID || !consentID || !sessionID) {
+        Logger.dump('Telemetry::track ignored: a required ID is not set: ' +
+            profileID + ' / ' + consentID + ' / ' + sessionID);
+        return;
+      }
+
+      var event = {};
+      event.pid = profileID;
+      event.cid = consentID;
+      event.sid = sessionID;
+      event.eid = getEventID();
+      event.geid = getGlobalEventID();
+      event.ts = getTimestamp();
+      event.name = name;
+      if (properties) {
+        event.props = properties;
+      }
+      enqueue(event);
+    } catch (e) {}
+  },
+
+  setProfileID : function(id) {
+    profileID = parseInt(id);
+    Logger.dump('Telemetry profileID: ' + id);
+  },
+
+  setConsentID : function(id) {
+    consentID = id;
+    Logger.dump('Telemetry consentID: ' + id);
+  },
+
+  setSessionID : function(id) {
+    sessionID = id;
+    Logger.dump('Telemetry sessionID: ' + id);
+  },
+
+  setGlobalEventID : function(id) {
+    globalEventID = id;
+    Logger.dump('Telemetry globalEventID: ' + id);
+  },
+
+  generateProfileID : function() {
+    return getRandomID();
+  },
+
+  /**
+   * For use when the browser is shutting down: manually process the queue.
+   */
+  processQueue : function(timer, synchronous) {
+    processQueue(synchronous);
+  }
+
+};
+
+
+var timer = Components.classes["@mozilla.org/timer;1"].createInstance(
+  Components.interfaces.nsITimer);
+timer.initWithCallback(Telemetry.processQueue, TELEMETRY_SEND_INTERVAL,
+                       Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
diff --git a/src/skin/requestpolicy.css b/src/skin/requestpolicy.css
index 0943ca4..a6d2a4b 100644
--- a/src/skin/requestpolicy.css
+++ b/src/skin/requestpolicy.css
@@ -195,4 +195,12 @@ toolbar[iconsize="small"][requestpolicyPermissive="true"] #requestpolicyToolbarB
   -moz-border-right-colors: #aaa #888;
   background-color: #eee;
   -moz-user-focus: ignore;
+}
+
+/*********************************************
+ * Research study
+ *********************************************/
+
+#requestpolicyParticipateInStudy {
+  color: #3950c5;
 }
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/requestpolicy.git



More information about the Pkg-mozext-commits mailing list