merge mozilla-central to mozilla-inbound. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sat, 03 Jun 2017 20:18:26 +0200
changeset 412684 31d8f2665ea0dd7c26b79b86656fc9f61feee26d
parent 412683 10328a332b658474c1626a3c6b77f4d0365a2c8e (current diff)
parent 412659 130efc657df7e7fe291cc42307f3eb3cb0484dfc (diff)
child 412685 6f99018495e75047836b845fa90e8289c7832303
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-central to mozilla-inbound. r=merge a=merge
devtools/client/framework/test/browser_source_map-no-race.js
devtools/client/framework/test/code_bundle_no_race.js
devtools/client/framework/test/code_bundle_no_race.js.map
devtools/client/framework/test/code_no_race.js
mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -376,31 +376,33 @@ this.PanelMultiView = class {
         this._viewStack.insertBefore(aNewMainView, this._viewStack.firstChild);
       }
     } else {
       this._mainViewContainer.appendChild(aNewMainView);
     }
   }
 
   showMainView() {
-    if (this.panelViews) {
-      this.showSubView(this._mainViewId);
-    } else {
-      if (this.showingSubView) {
-        let viewNode = this._currentSubView;
-        let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
-        viewNode.dispatchEvent(evt);
-
+    if (this.showingSubView) {
+      let viewNode = this._currentSubView;
+      let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
+      viewNode.dispatchEvent(evt);
+      if (this.panelViews) {
+        viewNode.removeAttribute("current");
+        this.showSubView(this._mainViewId);
+      } else {
         this._transitionHeight(() => {
           viewNode.removeAttribute("current");
           this._currentSubView = null;
           this.node.setAttribute("viewtype", "main");
         });
       }
+    }
 
+    if (!this.panelViews) {
       this._shiftMainView();
     }
   }
 
   showSubView(aViewId, aAnchor, aPreviousView) {
     const {document, window} = this;
     return (async () => {
       // Support passing in the node directly.
@@ -428,18 +430,18 @@ this.PanelMultiView = class {
         dwu = this._dwu;
         previousRect = previousViewNode.__lastKnownBoundingRect =
           dwu.getBoundsWithoutFlushing(previousViewNode);
         if (this.panelViews) {
           // Here go the measures that have the same caching lifetime as the width
           // of the main view, i.e. 'forever', during the instance lifetime.
           if (!this._mainViewWidth) {
             this._mainViewWidth = previousRect.width;
-            let top = dwu.getBoundsWithoutFlushing(previousViewNode.firstChild).top;
-            let bottom = dwu.getBoundsWithoutFlushing(previousViewNode.lastChild).bottom;
+            let top = dwu.getBoundsWithoutFlushing(previousViewNode.firstChild || previousViewNode).top;
+            let bottom = dwu.getBoundsWithoutFlushing(previousViewNode.lastChild || previousViewNode).bottom;
             this._viewVerticalPadding = previousRect.height - (bottom - top);
           }
           // Here go the measures that have the same caching lifetime as the height
           // of the main view, i.e. whilst the panel is shown and/ or visible.
           if (!this._mainViewHeight) {
             this._mainViewHeight = previousRect.height;
           }
         }
@@ -637,17 +639,17 @@ this.PanelMultiView = class {
           viewNode.setAttribute("current", true);
           this.node.setAttribute("viewtype", "subview");
           // Now that the subview is visible, we can check the height of the
           // description elements it contains.
           this.descriptionHeightWorkaround(viewNode);
         });
         this._shiftMainView(aAnchor);
       }
-    })();
+    })().catch(e => Cu.reportError(e));
   }
 
   /**
    * Applies the height transition for which <panelmultiview> is designed.
    *
    * The height transition involves two elements, the viewContainer and its only
    * immediate child the viewStack. In order for this to work correctly, the
    * viewContainer must have "overflow: hidden;" and the two elements must have
--- a/browser/components/preferences/in-content-new/preferences.js
+++ b/browser/components/preferences/in-content-new/preferences.js
@@ -146,32 +146,38 @@ function telemetryBucketForCategory(cate
 
 function onHashChange() {
   gotoPref();
 }
 
 function gotoPref(aCategory) {
   let categories = document.getElementById("categories");
   const kDefaultCategoryInternalName = "paneGeneral";
+  const kDefaultCategory = "general";
   let hash = document.location.hash;
 
   let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
   let breakIndex = category.indexOf("-");
   // Subcategories allow for selecting smaller sections of the preferences
   // until proper search support is enabled (bug 1353954).
   let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
   if (subcategory) {
     category = category.substring(0, breakIndex);
   }
   category = friendlyPrefCategoryNameToInternalName(category);
   if (category != "paneSearchResults") {
     gSearchResultsPane.searchInput.value = "";
     gSearchResultsPane.searchResultsCategory.hidden = true;
     gSearchResultsPane.findSelection.removeAllRanges();
     gSearchResultsPane.removeAllSearchTooltips();
+  } else if (!gSearchResultsPane.searchInput.value) {
+    // Something tried to send us to the search results pane without
+    // a query string. Default to the General pane instead.
+    category = kDefaultCategoryInternalName;
+    document.location.hash = kDefaultCategory;
   }
 
   // Updating the hash (below) or changing the selected category
   // will re-enter gotoPref.
   if (gLastHash == category && !subcategory)
     return;
   let item = categories.querySelector(".category[value=" + category + "]");
   if (!item) {
copy from browser/config/tooltool-manifests/linux64/releng.manifest
copy to browser/config/tooltool-manifests/linux64/base-toolchains.manifest
--- a/browser/config/tooltool-manifests/linux64/releng.manifest
+++ b/browser/config/tooltool-manifests/linux64/base-toolchains.manifest
@@ -11,19 +11,19 @@
     "size": 12072532,
     "digest": "3915f8ec396c56a8a92e6f9695b70f09ce9d1582359d1258e37e3fd43a143bc974410e4cfc27f500e095f34a8956206e0ebf799b7287f0f38def0d5e34ed71c9",
     "algorithm": "sha512",
     "filename": "gtk3.tar.xz",
     "setup": "setup.sh",
     "unpack": true
   },
   {
-    "version": "rustc 1.17.0 (56124baa9 2017-04-24) repack with cargo 0.19.0-beta.1 (03efb7fc8 2017-04-23)",
-    "size": 121925324,
-    "digest": "218ec1fd0a00b50fc3f0ce497d2bb9eb2230c1998a1c22cfe8f61ac44d9b57a081e12e8cd946e9d960a1683ed50d625d62f35f2db6fa6aab82454f96f32cd7b2",
+    "version": "rustc 1.15.1 (021bd294c 2017-02-08) repack",
+    "size": 110077036,
+    "digest": "8b99d058cc081f6ca2a3cc88c3ca9c15232961d2539774dacee35e2258955ad8fc4cb0af3b903a3e3f8a264ddecb3baae9256502ffc178a2823779284ace2bd8",
     "algorithm": "sha512",
     "filename": "rustc.tar.xz",
     "unpack": true
   },
   {
     "version": "sccache rev d3aa1116844b50c03015266d2f48235509fa7deb",
     "algorithm": "sha512",
     "visibility": "public",
--- a/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
+++ b/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
@@ -1,15 +1,21 @@
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] {
   border-bottom: 1px solid var(--panel-separator-color);
   padding-inline-start: 0;
   padding-inline-end: 6px;
   min-height: 3em;
 }
 
+/* Limit the size of the hidden description, since a deck takes the size of the biggest child */
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][selectedIndex="0"] #search-suggestions-hint,
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][selectedIndex="1"] #search-suggestions-question {
+  max-height: 5em;
+}
+
 /* Opt-in notification */
 
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-in"] {
   padding: 6px 0;
   padding-inline-start: 44px;
   background-color: hsla(210, 4%, 10%, 0.07);
   background-image: url("chrome://browser/skin/info.svg");
   background-clip: padding-box;
@@ -56,16 +62,24 @@
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button[anonid="search-suggestions-notification-enable"]:hover {
   background-color: hsl(93, 82%, 40%);
 }
 
 /* Opt-out hint */
 
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-out"] {
   font: message-box;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-box"] {
+  flex-basis: 100%;
 }
 
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-box"] > description {
   margin: auto;
   padding: 4px 8px;
   background-color: #ffeebe;
   border: 1px solid #ffdf81;
   border-radius: 4px;
@@ -83,16 +97,17 @@
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] > hbox[anonid="search-suggestions-opt-out"] > .ac-site-icon {
   transform: scale(0);
   animation-name: search-suggestions-hint-grow;
   animation-duration: 500ms;
   animation-delay: 500ms;
   animation-iteration-count: 1;
   animation-timing-function: ease-in-out;
   animation-fill-mode: forwards;
+  min-width: 16px;
 }
 
 @keyframes search-suggestions-hint-grow {
   0%   { transform: scale(0); }
   40%  { transform: scale(1.5); }
   60%  { transform: scale(1); }
   80%  { transform: scale(1.25); }
   100% { transform: scale(1); }
@@ -108,16 +123,23 @@
   width: 0;
   animation-name: search-suggestions-hint-typing;
   animation-duration: 500ms;
   animation-delay: 750ms;
   animation-iteration-count: 1;
   animation-fill-mode: forwards;
 }
 
+@media all and (max-width: 800px) {
+  /* Hide the typing animation block */
+  #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-typing"] {
+    display: none;
+  }
+}
+
 @keyframes search-suggestions-hint-typing {
   from { width: 0; }
   to   { width: 8ch; }
 }
 
 #PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-box"] {
   opacity: 0;
   animation-duration: 250ms;
--- a/devtools/client/framework/source-map-url-service.js
+++ b/devtools/client/framework/source-map-url-service.js
@@ -6,39 +6,29 @@
 /**
  * A simple service to track source actors and keep a mapping between
  * original URLs and objects holding the source actor's ID (which is
  * used as a cookie by the devtools-source-map service) and the source
  * map URL.
  *
  * @param {object} target
  *        The object the toolbox is debugging.
- * @param {object} threadClient
- *        The toolbox's thread client
  * @param {SourceMapService} sourceMapService
  *        The devtools-source-map functions
  */
-function SourceMapURLService(target, threadClient, sourceMapService) {
+function SourceMapURLService(target, sourceMapService) {
   this._target = target;
   this._sourceMapService = sourceMapService;
   this._urls = new Map();
 
   this._onSourceUpdated = this._onSourceUpdated.bind(this);
   this.reset = this.reset.bind(this);
 
   target.on("source-updated", this._onSourceUpdated);
   target.on("will-navigate", this.reset);
-
-  // Start fetching the sources now.
-  this._loadingPromise = new Promise(resolve => {
-    threadClient.getSources(({sources}) => {
-      // Just ignore errors.
-      resolve(sources);
-    });
-  });
 }
 
 /**
  * Reset the service.  This flushes the internal cache.
  */
 SourceMapURLService.prototype.reset = function () {
   this._sourceMapService.clearSourceMaps();
   this._urls.clear();
@@ -80,24 +70,16 @@ SourceMapURLService.prototype._onSourceU
  * @param {number} line
  *        The line number to map.
  * @param {number} column
  *        The column number to map.
  * @return Promise
  *        A promise resolving either to the original location, or null.
  */
 SourceMapURLService.prototype.originalPositionFor = async function (url, line, column) {
-  // Ensure the sources are loaded before replying.
-  await this._loadingPromise;
-
-  // Maybe we were shut down while waiting.
-  if (!this._urls) {
-    return null;
-  }
-
   const urlInfo = this._urls.get(url);
   if (!urlInfo) {
     return null;
   }
   // Call getOriginalURLs to make sure the source map has been
   // fetched.  We don't actually need the result of this though.
   await this._sourceMapService.getOriginalURLs(urlInfo);
   const location = { sourceId: urlInfo.id, line, column, sourceUrl: url };
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -8,26 +8,23 @@ support-files =
   browser_toolbox_sidebar_tool.xul
   browser_toolbox_window_title_changes_page.html
   browser_toolbox_window_title_frame_select_page.html
   code_binary_search.coffee
   code_binary_search.js
   code_binary_search.map
   code_binary_search_absolute.js
   code_binary_search_absolute.map
-  code_bundle_no_race.js
-  code_bundle_no_race.js.map
   code_bundle_reload_1.js
   code_bundle_reload_1.js.map
   code_bundle_reload_2.js
   code_bundle_reload_2.js.map
   code_inline_bundle.js
   code_inline_original.js
   code_math.js
-  code_no_race.js
   code_reload_1.js
   code_reload_2.js
   doc_empty-tab-01.html
   doc_reload.html
   head.js
   shared-head.js
   shared-redux-head.js
   helper_disable_cache.js
@@ -50,17 +47,16 @@ support-files =
 [browser_keybindings_01.js]
 [browser_keybindings_02.js]
 [browser_keybindings_03.js]
 [browser_menu_api.js]
 [browser_new_activation_workflow.js]
 [browser_source_map-01.js]
 [browser_source_map-absolute.js]
 [browser_source_map-inline.js]
-[browser_source_map-no-race.js]
 [browser_source_map-reload.js]
 [browser_target_from_url.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_toolbox_custom_host.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
--- a/devtools/client/framework/test/browser_devtools_api.js
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -9,25 +9,27 @@
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
 
 // When running in a standalone directory, we get this error
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.doc is undefined");
 
 // Tests devtools API
 
+"use strict";
+
 const toolId1 = "test-tool-1";
 const toolId2 = "test-tool-2";
 
 function test() {
   addTab("about:blank").then(runTests1);
 }
 
 // Test scenario 1: the tool definition build method returns a promise.
-function runTests1(aTab) {
+function runTests1(tab) {
   let toolDefinition = {
     id: toolId1,
     isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
     label: "someLabel",
     build: function (iframeWindow, toolbox) {
       let panel = createTestPanel(iframeWindow, toolbox);
@@ -46,37 +48,37 @@ function runTests1(aTab) {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
   let events = {};
 
   // Check events on the gDevTools and toolbox objects.
   gDevTools.once(toolId1 + "-init", (event, toolbox, iframe) => {
     ok(iframe, "iframe argument available");
 
-    toolbox.once(toolId1 + "-init", (event, iframe) => {
-      ok(iframe, "iframe argument available");
-      events["init"] = true;
+    toolbox.once(toolId1 + "-init", (innerEvent, innerIframe) => {
+      ok(innerIframe, "innerIframe argument available");
+      events.init = true;
     });
   });
 
   gDevTools.once(toolId1 + "-ready", (event, toolbox, panel) => {
     ok(panel, "panel argument available");
 
-    toolbox.once(toolId1 + "-ready", (event, panel) => {
-      ok(panel, "panel argument available");
-      events["ready"] = true;
+    toolbox.once(toolId1 + "-ready", (innerEvent, innerPanel) => {
+      ok(innerPanel, "innerPanel argument available");
+      events.ready = true;
     });
   });
 
   gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
     is(toolbox.target, target, "toolbox target is correct");
     is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
 
-    ok(events["init"], "init event fired");
-    ok(events["ready"], "ready event fired");
+    ok(events.init, "init event fired");
+    ok(events.ready, "ready event fired");
 
     gDevTools.unregisterTool(toolId1);
 
     // Wait for unregisterTool to select the next tool before calling runTests2,
     // otherwise we will receive the wrong select event when waiting for
     // unregisterTool to select the next tool in continueTests below.
     toolbox.once("select", runTests2);
   });
@@ -105,47 +107,47 @@ function runTests2() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
   let events = {};
 
   // Check events on the gDevTools and toolbox objects.
   gDevTools.once(toolId2 + "-init", (event, toolbox, iframe) => {
     ok(iframe, "iframe argument available");
 
-    toolbox.once(toolId2 + "-init", (event, iframe) => {
-      ok(iframe, "iframe argument available");
-      events["init"] = true;
+    toolbox.once(toolId2 + "-init", (innerEvent, innerIframe) => {
+      ok(innerIframe, "innerIframe argument available");
+      events.init = true;
     });
   });
 
   gDevTools.once(toolId2 + "-build", (event, toolbox, panel, iframe) => {
     ok(panel, "panel argument available");
 
-    toolbox.once(toolId2 + "-build", (event, panel, iframe) => {
-      ok(panel, "panel argument available");
-      events["build"] = true;
+    toolbox.once(toolId2 + "-build", (innerEvent, innerPanel, innerIframe) => {
+      ok(innerPanel, "innerPanel argument available");
+      events.build = true;
     });
   });
 
   gDevTools.once(toolId2 + "-ready", (event, toolbox, panel) => {
     ok(panel, "panel argument available");
 
-    toolbox.once(toolId2 + "-ready", (event, panel) => {
-      ok(panel, "panel argument available");
-      events["ready"] = true;
+    toolbox.once(toolId2 + "-ready", (innerEvent, innerPanel) => {
+      ok(innerPanel, "innerPanel argument available");
+      events.ready = true;
     });
   });
 
   gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
     is(toolbox.target, target, "toolbox target is correct");
     is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
 
-    ok(events["init"], "init event fired");
-    ok(events["build"], "build event fired");
-    ok(events["ready"], "ready event fired");
+    ok(events.init, "init event fired");
+    ok(events.build, "build event fired");
+    ok(events.ready, "ready event fired");
 
     continueTests(toolbox);
   });
 }
 
 var continueTests = Task.async(function* (toolbox, panel) {
   ok(toolbox.getCurrentPanel(), "panel value is correct");
   is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
@@ -188,16 +190,17 @@ var continueTests = Task.async(function*
   ok(toolbox.isToolRegistered(toolId2),
     "Toolbox: The tool is registered");
   ok(gDevTools.getToolDefinitionMap().has(toolId2),
     "The tool is registered");
 
   info("Unregistering tool");
   gDevTools.unregisterTool(toolId2);
 
+  info("Destroying toolbox");
   destroyToolbox(toolbox);
 });
 
 function destroyToolbox(toolbox) {
   toolbox.destroy().then(function () {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     ok(gDevTools._toolboxes.get(target) == null, "gDevTools doesn't know about target");
     ok(toolbox.target == null, "toolbox doesn't know about target.");
deleted file mode 100644
--- a/devtools/client/framework/test/browser_source_map-no-race.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Test that the source map service doesn't race against source
-// reporting.
-
-"use strict";
-
-const JS_URL = URL_ROOT + "code_bundle_no_race.js";
-
-const PAGE_URL = `data:text/html,
-<!doctype html>
-
-<html>
-  <script src="${JS_URL}"></script>
-  <head>
-    <meta charset="utf-8"/>
-    <title>Empty test page to test race case</title>
-  </head>
-
-  <body>
-  </body>
-
-</html>`;
-
-const ORIGINAL_URL = "webpack:///code_no_race.js";
-
-const GENERATED_LINE = 84;
-const ORIGINAL_LINE = 11;
-
-add_task(function* () {
-  // Start with the empty page, then navigate, so that we can properly
-  // listen for new sources arriving.
-  const toolbox = yield openNewTabAndToolbox(PAGE_URL, "webconsole");
-  const service = toolbox.sourceMapURLService;
-
-  info(`checking original location for ${JS_URL}:${GENERATED_LINE}`);
-  let newLoc = yield service.originalPositionFor(JS_URL, GENERATED_LINE);
-  is(newLoc.sourceUrl, ORIGINAL_URL, "check mapped URL");
-  is(newLoc.line, ORIGINAL_LINE, "check mapped line number");
-});
deleted file mode 100644
--- a/devtools/client/framework/test/code_bundle_no_race.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/******/ (function(modules) { // webpackBootstrap
-/******/ 	// The module cache
-/******/ 	var installedModules = {};
-/******/
-/******/ 	// The require function
-/******/ 	function __webpack_require__(moduleId) {
-/******/
-/******/ 		// Check if module is in cache
-/******/ 		if(installedModules[moduleId]) {
-/******/ 			return installedModules[moduleId].exports;
-/******/ 		}
-/******/ 		// Create a new module (and put it into the cache)
-/******/ 		var module = installedModules[moduleId] = {
-/******/ 			i: moduleId,
-/******/ 			l: false,
-/******/ 			exports: {}
-/******/ 		};
-/******/
-/******/ 		// Execute the module function
-/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
-/******/ 		// Flag the module as loaded
-/******/ 		module.l = true;
-/******/
-/******/ 		// Return the exports of the module
-/******/ 		return module.exports;
-/******/ 	}
-/******/
-/******/
-/******/ 	// expose the modules object (__webpack_modules__)
-/******/ 	__webpack_require__.m = modules;
-/******/
-/******/ 	// expose the module cache
-/******/ 	__webpack_require__.c = installedModules;
-/******/
-/******/ 	// identity function for calling harmony imports with the correct context
-/******/ 	__webpack_require__.i = function(value) { return value; };
-/******/
-/******/ 	// define getter function for harmony exports
-/******/ 	__webpack_require__.d = function(exports, name, getter) {
-/******/ 		if(!__webpack_require__.o(exports, name)) {
-/******/ 			Object.defineProperty(exports, name, {
-/******/ 				configurable: false,
-/******/ 				enumerable: true,
-/******/ 				get: getter
-/******/ 			});
-/******/ 		}
-/******/ 	};
-/******/
-/******/ 	// getDefaultExport function for compatibility with non-harmony modules
-/******/ 	__webpack_require__.n = function(module) {
-/******/ 		var getter = module && module.__esModule ?
-/******/ 			function getDefault() { return module['default']; } :
-/******/ 			function getModuleExports() { return module; };
-/******/ 		__webpack_require__.d(getter, 'a', getter);
-/******/ 		return getter;
-/******/ 	};
-/******/
-/******/ 	// Object.prototype.hasOwnProperty.call
-/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
-/******/ 	// __webpack_public_path__
-/******/ 	__webpack_require__.p = "";
-/******/
-/******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 0);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Original source code for the inline source map test.
-// The generated file was made with
-//    webpack --devtool source-map code_no_race.js code_bundle_no_race.js
-
-
-
-function f() {
-  console.log("anything will do");
-}
-
-f();
-
-
-/***/ })
-/******/ ]);
-//# sourceMappingURL=code_bundle_no_race.js.map
\ No newline at end of file
deleted file mode 100644
--- a/devtools/client/framework/test/code_bundle_no_race.js.map
+++ /dev/null
@@ -1,1 +0,0 @@
-{"version":3,"sources":["webpack:///webpack/bootstrap 40c4319d19d88c63024f","webpack:///./code_no_race.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA","file":"code_bundle_no_race.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 40c4319d19d88c63024f","/* Any copyright is dedicated to the Public Domain.\n http://creativecommons.org/publicdomain/zero/1.0/ */\n\n// Original source code for the inline source map test.\n// The generated file was made with\n//    webpack --devtool source-map code_no_race.js code_bundle_no_race.js\n\n\"use strict\";\n\nfunction f() {\n  console.log(\"anything will do\");\n}\n\nf();\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./code_no_race.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""}
\ No newline at end of file
deleted file mode 100644
--- a/devtools/client/framework/test/code_no_race.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Original source code for the inline source map test.
-// The generated file was made with
-//    webpack --devtool source-map code_no_race.js code_bundle_no_race.js
-
-"use strict";
-
-function f() {
-  console.log("anything will do");
-}
-
-f();
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -555,18 +555,17 @@ Toolbox.prototype = {
   get sourceMapURLService() {
     if (this._sourceMapURLService) {
       return this._sourceMapURLService;
     }
     let sourceMaps = this.sourceMapService;
     if (!sourceMaps) {
       return null;
     }
-    this._sourceMapURLService = new SourceMapURLService(this._target, this.threadClient,
-                                                        sourceMaps);
+    this._sourceMapURLService = new SourceMapURLService(this._target, sourceMaps);
     return this._sourceMapURLService;
   },
 
   // Return HostType id for telemetry
   _getTelemetryHostId: function () {
     switch (this.hostType) {
       case Toolbox.HostType.BOTTOM: return 0;
       case Toolbox.HostType.SIDE: return 1;
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -137,17 +137,26 @@ function StorageUI(front, target, panelW
   let key = L10N.getStr("storage.filter.key");
   shortcuts.on(key, (name, event) => {
     event.preventDefault();
     this.searchBox.focus();
   });
 
   this.front.listStores().then(storageTypes => {
     this.populateStorageTree(storageTypes);
-  }).then(null, console.error);
+  }).catch(e => {
+    if (!this._toolbox || this._toolbox._destroyer) {
+      // The toolbox is in the process of being destroyed... in this case throwing here
+      // is expected and normal so let's ignore the error.
+      return;
+    }
+
+    // The toolbox is open so the error is unexpected and real so let's log it.
+    console.error(e);
+  });
 
   this.onUpdate = this.onUpdate.bind(this);
   this.front.on("stores-update", this.onUpdate);
   this.onCleared = this.onCleared.bind(this);
   this.front.on("stores-cleared", this.onCleared);
 
   this.handleKeypress = this.handleKeypress.bind(this);
   this._panelDoc.addEventListener("keypress", this.handleKeypress);
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -296,16 +296,19 @@ bool nsContentUtils::sIsCustomElementsEn
 bool nsContentUtils::sSendPerformanceTimingNotifications = false;
 bool nsContentUtils::sUseActivityCursor = false;
 bool nsContentUtils::sAnimationsAPICoreEnabled = false;
 bool nsContentUtils::sAnimationsAPIElementAnimateEnabled = false;
 bool nsContentUtils::sGetBoxQuadsEnabled = false;
 bool nsContentUtils::sSkipCursorMoveForSameValueSet = false;
 bool nsContentUtils::sRequestIdleCallbackEnabled = false;
 bool nsContentUtils::sLowerNetworkPriority = false;
+#ifndef RELEASE_OR_BETA
+bool nsContentUtils::sBypassCSSOMOriginCheck = false;
+#endif
 
 int32_t nsContentUtils::sPrivacyMaxInnerWidth = 1000;
 int32_t nsContentUtils::sPrivacyMaxInnerHeight = 1000;
 
 nsContentUtils::UserInteractionObserver*
 nsContentUtils::sUserInteractionObserver = nullptr;
 
 uint32_t nsContentUtils::sHandlingInputTimeout = 1000;
@@ -710,16 +713,20 @@ nsContentUtils::Init()
 
   Preferences::AddBoolVarCache(&sSkipCursorMoveForSameValueSet,
                                "dom.input.skip_cursor_move_for_same_value_set",
                                true);
 
   Preferences::AddBoolVarCache(&sRequestIdleCallbackEnabled,
                                "dom.requestIdleCallback.enabled", false);
 
+#ifndef RELEASE_OR_BETA
+  sBypassCSSOMOriginCheck = getenv("MOZ_BYPASS_CSSOM_ORIGIN_CHECK");
+#endif
+
   Preferences::AddBoolVarCache(&sLowerNetworkPriority,
                                "privacy.trackingprotection.lower_network_priority", false);
 
   Element::InitCCCallbacks();
 
   Unused << nsRFPService::GetOrCreate();
 
   nsCOMPtr<nsIUUIDGenerator> uuidGenerator =
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2227,16 +2227,31 @@ public:
    * Returns true if the requestIdleCallback API should be enabled.
    */
   static bool RequestIdleCallbackEnabled()
   {
     return sRequestIdleCallbackEnabled;
   }
 
   /**
+   * Returns true if CSSOM origin check should be skipped for WebDriver
+   * based crawl to be able to collect data from cross-origin CSS style
+   * sheets. This can be enabled by setting environment variable
+   * MOZ_BYPASS_CSSOM_ORIGIN_CHECK.
+   */
+  static bool BypassCSSOMOriginCheck()
+  {
+#ifdef RELEASE_OR_BETA
+    return false;
+#else
+    return sBypassCSSOMOriginCheck;
+#endif
+  }
+
+  /**
    * Return true if this doc is controlled by a ServiceWorker.
    */
   static bool IsControlledByServiceWorker(nsIDocument* aDocument);
 
   /**
    * Fire mutation events for changes caused by parsing directly into a
    * context node.
    *
@@ -3092,16 +3107,19 @@ private:
   static bool sSendPerformanceTimingNotifications;
   static bool sUseActivityCursor;
   static bool sAnimationsAPICoreEnabled;
   static bool sAnimationsAPIElementAnimateEnabled;
   static bool sGetBoxQuadsEnabled;
   static bool sSkipCursorMoveForSameValueSet;
   static bool sRequestIdleCallbackEnabled;
   static bool sLowerNetworkPriority;
+#ifndef RELEASE_OR_BETA
+  static bool sBypassCSSOMOriginCheck;
+#endif
   static uint32_t sCookiesLifetimePolicy;
   static uint32_t sCookiesBehavior;
 
   static int32_t sPrivacyMaxInnerWidth;
   static int32_t sPrivacyMaxInnerHeight;
 
   class UserInteractionObserver;
   static UserInteractionObserver* sUserInteractionObserver;
--- a/dom/gamepad/windows/WindowsGamepad.cpp
+++ b/dom/gamepad/windows/WindowsGamepad.cpp
@@ -14,16 +14,17 @@
 #include <hidsdi.h>
 #include <stdio.h>
 #include <xinput.h>
 
 #include "nsIComponentManager.h"
 #include "nsITimer.h"
 #include "nsTArray.h"
 #include "nsThreadUtils.h"
+#include "WinUtils.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Services.h"
 
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/dom/GamepadPlatformService.h"
 
 namespace {
@@ -331,17 +332,17 @@ private:
 
 HWND sHWnd = nullptr;
 
 static void
 DirectInputMessageLoopOnceCallback(nsITimer *aTimer, void* aClosure)
 {
   MOZ_ASSERT(NS_GetCurrentThread() == gMonitorThread);
   MSG msg;
-  while (PeekMessageW(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) {
+  while (widget::WinUtils::PeekMessage(&msg, sHWnd, 0, 0, PM_REMOVE) > 0) {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
   }
   aTimer->Cancel();
   if (!sIsShutdown) {
     aTimer->InitWithFuncCallback(DirectInputMessageLoopOnceCallback,
                                  nullptr, kWindowsGamepadPollInterval,
                                  nsITimer::TYPE_ONE_SHOT);
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -179,17 +179,16 @@
 #include "nsPluginHost.h"
 #include "nsPluginTags.h"
 #include "nsIBlocklistService.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "nsHostObjectProtocolHandler.h"
 #include "nsICaptivePortalService.h"
 #include "nsIObjectLoadingContent.h"
-#include "ProfilerParent.h"
 
 #include "nsIBidiKeyboard.h"
 
 #include "nsLayoutStylesheetCache.h"
 
 #include "ContentPrefs.h"
 #include "mozilla/Sprintf.h"
 
@@ -252,16 +251,17 @@
 #endif
 
 #ifdef ACCESSIBILITY
 #include "nsAccessibilityService.h"
 #endif
 
 #ifdef MOZ_GECKO_PROFILER
 #include "nsIProfiler.h"
+#include "ProfilerParent.h"
 #endif
 
 // For VP9Benchmark::sBenchmarkFpsPref
 #include "Benchmark.h"
 
 static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
 
 #if defined(XP_WIN)
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -2514,16 +2514,19 @@ TabParent::TryCacheDPIAndScale()
 
 already_AddRefed<nsIWidget>
 TabParent::GetWidget() const
 {
   if (!mFrameElement) {
     return nullptr;
   }
   nsCOMPtr<nsIWidget> widget = nsContentUtils::WidgetForContent(mFrameElement);
+  if (!widget) {
+    widget = nsContentUtils::WidgetForDocument(mFrameElement->OwnerDoc());
+  }
   return widget.forget();
 }
 
 void
 TabParent::ApzAwareEventRoutingToChild(ScrollableLayerGuid* aOutTargetGuid,
                                        uint64_t* aOutInputBlockId,
                                        nsEventStatus* aOutApzResponse)
 {
--- a/dom/plugins/ipc/PluginModuleParent.cpp
+++ b/dom/plugins/ipc/PluginModuleParent.cpp
@@ -29,17 +29,16 @@
 #include "nsIXULRuntime.h"
 #include "nsNPAPIPlugin.h"
 #include "nsPrintfCString.h"
 #include "prsystem.h"
 #include "prclist.h"
 #include "PluginQuirks.h"
 #include "gfxPlatform.h"
 #include "GeckoProfiler.h"
-#include "ProfilerParent.h"
 #include "nsPluginTags.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/layers/TextureClientRecycleAllocator.h"
 
 #ifdef XP_WIN
 #include "mozilla/plugins/PluginSurfaceParent.h"
 #include "mozilla/widget/AudioSession.h"
 #include "PluginHangUIParent.h"
@@ -48,16 +47,20 @@
 
 #ifdef MOZ_WIDGET_GTK
 #include <glib.h>
 #elif XP_MACOSX
 #include "PluginInterposeOSX.h"
 #include "PluginUtilsOSX.h"
 #endif
 
+#ifdef MOZ_GECKO_PROFILER
+#include "ProfilerParent.h"
+#endif
+
 using base::KillProcess;
 
 using mozilla::PluginLibrary;
 using mozilla::ipc::MessageChannel;
 using mozilla::ipc::GeckoChildProcessHost;
 
 using namespace mozilla;
 using namespace mozilla::plugins;
--- a/gfx/ipc/GPUChild.cpp
+++ b/gfx/ipc/GPUChild.cpp
@@ -15,17 +15,20 @@
 #include "mozilla/dom/MemoryReportRequest.h"
 #include "mozilla/gfx/gfxVars.h"
 #if defined(XP_WIN)
 # include "mozilla/gfx/DeviceManagerDx.h"
 #endif
 #include "mozilla/ipc/CrashReporterHost.h"
 #include "mozilla/layers/LayerTreeOwnerTracker.h"
 #include "mozilla/Unused.h"
+
+#ifdef MOZ_GECKO_PROFILER
 #include "ProfilerParent.h"
+#endif
 
 namespace mozilla {
 namespace gfx {
 
 using namespace layers;
 
 GPUChild::GPUChild(GPUProcessHost* aHost)
  : mHost(aHost),
--- a/ipc/glue/WindowsMessageLoop.cpp
+++ b/ipc/glue/WindowsMessageLoop.cpp
@@ -18,16 +18,17 @@
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/ipc/ProtocolUtils.h"
 #include "mozilla/PaintTracker.h"
 
 using namespace mozilla;
 using namespace mozilla::ipc;
 using namespace mozilla::ipc::windows;
+using namespace mozilla::widget;
 
 /**
  * The Windows-only code below exists to solve a general problem with deadlocks
  * that we experience when sending synchronous IPC messages to processes that
  * contain native windows (i.e. HWNDs). Windows (the OS) sends synchronous
  * messages between parent and child HWNDs in multiple circumstances (e.g.
  * WM_PARENTNOTIFY, WM_NCACTIVATE, etc.), even when those HWNDs are controlled
  * by different threads or different processes. Thus we can very easily end up
@@ -814,17 +815,17 @@ MessageChannel::SpinInternalEventLoop()
     {
       MonitorAutoLock lock(*mMonitor);
       if (!Connected()) {
         return;
       }
     }
 
     // Retrieve window or thread messages
-    if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
+    if (WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
       // The child UI should have been destroyed before the app is closed, in
       // which case, we should never get this here.
       if (msg.message == WM_QUIT) {
           NS_ERROR("WM_QUIT received in SpinInternalEventLoop!");
       } else {
           TranslateMessage(&msg);
           ::DispatchMessageW(&msg);
           return;
@@ -904,22 +905,22 @@ NeuteredWindowRegion::PumpOnce()
 {
   if (!gWindowHook) {
     // This should be a no-op if nothing has been neutered.
     return;
   }
 
   MSG msg = {0};
   // Pump any COM messages so that we don't hang due to STA marshaling.
-  if (gCOMWindow && ::PeekMessageW(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
+  if (gCOMWindow && WinUtils::PeekMessage(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
       ::TranslateMessage(&msg);
       ::DispatchMessageW(&msg);
   }
   // Expunge any nonqueued messages on the current thread.
-  ::PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE);
+  WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_NOREMOVE);
 }
 
 DeneuteredWindowRegion::DeneuteredWindowRegion(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_IN_IMPL)
   : mReneuter(gWindowHook != NULL)
 {
   MOZ_GUARD_OBJECT_NOTIFIER_INIT;
   if (mReneuter) {
     StopNeutering();
@@ -1130,28 +1131,28 @@ MessageChannel::WaitForSyncNotify(bool a
       // "nonqueued" messages that are pending before returning. If we have
       // "nonqueued" messages pending then we should have switched out all the
       // window procedures above. In that case this PeekMessage call won't
       // actually cause any mozilla code (or plugin code) to run.
 
       // We have to manually pump all COM messages *after* looking at the queue
       // queue status but before yielding our thread below.
       if (gCOMWindow) {
-        if (PeekMessageW(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
+        if (WinUtils::PeekMessage(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
           TranslateMessage(&msg);
           ::DispatchMessageW(&msg);
         }
       }
 
       // If the following PeekMessage call fails to return a message for us (and
       // returns false) and we didn't run any "nonqueued" messages then we must
       // have woken up for a message designated for a window in another thread.
       // If we loop immediately then we could enter a tight loop, so we'll give
       // up our time slice here to let the child process its message.
-      if (!PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE) &&
+      if (!WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_NOREMOVE) &&
           !haveSentMessagesPending) {
         // Message was for child, we should wait a bit.
         SwitchToThread();
       }
     }
   }
 
   if (timerId) {
@@ -1257,25 +1258,25 @@ MessageChannel::WaitForInterruptNotify()
     }
 
     // See MessageChannel's WaitFor*Notify for details.
     bool haveSentMessagesPending =
       (HIWORD(GetQueueStatus(QS_SENDMESSAGE)) & QS_SENDMESSAGE) != 0;
 
     // Run all COM messages *after* looking at the queue status.
     if (gCOMWindow) {
-        if (PeekMessageW(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
+        if (WinUtils::PeekMessage(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
             TranslateMessage(&msg);
             ::DispatchMessageW(&msg);
         }
     }
 
     // PeekMessage markes the messages as "old" so that they don't wake up
     // MsgWaitForMultipleObjects every time.
-    if (!PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE) &&
+    if (!WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_NOREMOVE) &&
         !haveSentMessagesPending) {
       // Message was for child, we should wait a bit.
       SwitchToThread();
     }
   }
 
   if (timerId) {
     KillTimer(nullptr, timerId);
new file mode 100644
--- /dev/null
+++ b/layout/generic/crashtests/1368617-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<style>
+.x:first-line { letter-spacing: 1px; }
+.x:first-letter { float: left; }
+</style>
+<script>document.documentElement.offsetTop;</script>
+<p class=x>&nbsp;</p>
+<script>document.documentElement.offsetTop;</script>
+<a href=""><svg></svg></a>
--- a/layout/generic/crashtests/crashtests.list
+++ b/layout/generic/crashtests/crashtests.list
@@ -648,8 +648,9 @@ load 1278461-1.html
 load 1278461-2.html
 load 1281102.html
 load 1297427-non-equal-centers.html
 load 1304441.html
 load 1316649.html
 load 1349650.html
 asserts-if(browserIsRemote,0-5) load 1349816-1.html # bug 1350352
 load 1367413-1.html
+load 1368617-1.html
--- a/layout/generic/nsFirstLetterFrame.cpp
+++ b/layout/generic/nsFirstLetterFrame.cpp
@@ -319,17 +319,21 @@ nsFirstLetterFrame::CreateContinuationFo
   nsContainerFrame* parent = placeholderFrame->GetParent();
 
   nsIFrame* continuation = presShell->FrameConstructor()->
     CreateContinuingFrame(aPresContext, aChild, parent, aIsFluid);
 
   // The continuation will have gotten the first letter style from its
   // prev continuation, so we need to repair the style context so it
   // doesn't have the first letter styling.
-  nsStyleContext* parentSC = this->StyleContext()->GetParentAllowServo();
+  //
+  // Note that getting parent frame's style context is different from getting
+  // this frame's style context's parent in the presence of ::first-line,
+  // which we do want the continuation to inherit from.
+  nsStyleContext* parentSC = parent->StyleContext();
   if (parentSC) {
     RefPtr<nsStyleContext> newSC;
     newSC = presShell->StyleSet()->ResolveStyleForFirstLetterContinuation(parentSC);
     continuation->SetStyleContext(newSC);
     nsLayoutUtils::MarkDescendantsDirty(continuation);
   }
 
   //XXX Bidi may not be involved but we have to use the list name
--- a/layout/ipc/RenderFrameParent.cpp
+++ b/layout/ipc/RenderFrameParent.cpp
@@ -212,16 +212,19 @@ RenderFrameParent::AttachLayerManager()
 {
   RefPtr<LayerManager> lm;
 
   if (mFrameLoader) {
     nsIContent* content = mFrameLoader->GetOwnerContent();
     if (content) {
       lm = nsContentUtils::LayerManagerForContent(content);
     }
+    if (!lm) {
+      lm = GetFrom(mFrameLoader);
+    }
   }
 
   // Perhaps the document containing this frame currently has no presentation?
   if (lm && lm->GetCompositorBridgeChild() && lm != mLayerManager) {
     mLayersConnected = lm->GetCompositorBridgeChild()->SendAdoptChild(mLayersId);
     FrameLayerBuilder::InvalidateAllLayers(lm);
   }
 
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -89,20 +89,20 @@ fails-if(styloVsGecko) != 82711-1-ref.ht
 fails-if(styloVsGecko) != 82711-1-ref.html 82711-3-ref.html
 fails-if(styloVsGecko) != 82711-2-ref.html 82711-3-ref.html
 fuzzy-if(Android,4,1) == 84400-1.html 84400-1-ref.html
 fuzzy-if(skiaContent,2,13) == 84400-2.html 84400-2-ref.html
 == 97777-1.html 97777-1-ref.html
 == 97777-2.html 97777-2-ref.html
 == 98223-1.html 98223-1-ref.html
 == 98223-2.html 98223-2-ref.html
-fails-if(styloVsGecko||stylo) == 99850-1a.html 99850-1-ref.html
+== 99850-1a.html 99850-1-ref.html
 random == 99850-1b.html 99850-1-ref.html # bug 471629
-fails-if(styloVsGecko||stylo) == 99850-1c.html 99850-1-ref.html
-fails-if(styloVsGecko||stylo) == 99850-1d.html 99850-1-ref.html
+== 99850-1c.html 99850-1-ref.html
+== 99850-1d.html 99850-1-ref.html
 == 105030-1.html 105030-1-ref.html
 == 109735-1.html 109735-1-ref.html
 == 116882-1.html 116882-1-ref.html
 == 120834-1a.html 120834-1-ref.html
 == 120834-2a.html 120834-2-ref.html
 == 120834-2b.html 120834-2-ref.html
 == 120834-2c.html 120834-2-ref.html
 == 120834-2d.html 120834-2-ref.html
@@ -1582,18 +1582,18 @@ fuzzy-if(Android,3,256) == 582037-2a.htm
 fuzzy-if(Android,3,256) == 582037-2b.html 582037-2-ref.html
 asserts(1-2) asserts-if(styloVsGecko,4) == 582146-1.html about:blank
 == 582476-1.svg 582476-1-ref.svg
 == 584400-dash-length.svg 584400-dash-length-ref.svg
 == 584699-1.html 584699-1-ref.html
 fuzzy-if(Android,2,48) == 585598-2.xhtml 585598-2-ref.xhtml
 == 586400-1.html 586400-1-ref.html
 fuzzy-if(d2d,52,1051) == 586683-1.html 586683-1-ref.html
-fails-if(styloVsGecko||stylo) == 589615-1a.xhtml 589615-1-ref.html
-fails-if(styloVsGecko||stylo) == 589615-1b.html 589615-1-ref.html
+== 589615-1a.xhtml 589615-1-ref.html
+== 589615-1b.html 589615-1-ref.html
 == 589672-1.html 589672-1-ref.html
 != 589682-1.html 589682-1-notref.html
 pref(dom.meta-viewport.enabled,true) skip-if(Android) == 593243-1.html 593243-1-ref.html # bug 593168
 pref(dom.meta-viewport.enabled,true) skip-if(Android) == 593243-2.html 593243-2-ref.html # bug 593168
 == 593544-1.html 593544-1-ref.html
 random-if(Android) == 594333-1.html 594333-1-ref.html
 == 594624-1.html 594624-1-ref.html
 == 594737-1.html 594737-1-ref.html
--- a/layout/style/ServoBindings.cpp
+++ b/layout/style/ServoBindings.cpp
@@ -28,16 +28,17 @@
 #include "nsIDocumentInlines.h"
 #include "nsIFrame.h"
 #include "nsINode.h"
 #include "nsIPresShell.h"
 #include "nsIPresShellInlines.h"
 #include "nsIPrincipal.h"
 #include "nsIURI.h"
 #include "nsFontMetrics.h"
+#include "nsHTMLStyleSheet.h"
 #include "nsMappedAttributes.h"
 #include "nsMediaFeatures.h"
 #include "nsNameSpaceManager.h"
 #include "nsNetUtil.h"
 #include "nsRuleNode.h"
 #include "nsString.h"
 #include "nsStyleStruct.h"
 #include "nsStyleUtil.h"
@@ -443,54 +444,88 @@ Gecko_GetSMILOverrideDeclarationBlock(Ra
     // XXX This can happen when nodes are adopted from a Gecko-style-backend
     //     document into a Servo-style-backend document.  See bug 1330051.
     NS_WARNING("stylo: requesting a Gecko declaration block?");
     return nullptr;
   }
   return decl->AsServo()->RefRawStrong();
 }
 
-RawServoDeclarationBlockStrongBorrowedOrNull
-Gecko_GetHTMLPresentationAttrDeclarationBlock(RawGeckoElementBorrowed aElement)
+const RawServoDeclarationBlockStrong*
+AsRefRawStrong(const RefPtr<RawServoDeclarationBlock>& aDecl)
 {
   static_assert(sizeof(RefPtr<RawServoDeclarationBlock>) ==
                 sizeof(RawServoDeclarationBlockStrong),
                 "RefPtr should just be a pointer");
+  return reinterpret_cast<const RawServoDeclarationBlockStrong*>(&aDecl);
+}
+
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetHTMLPresentationAttrDeclarationBlock(RawGeckoElementBorrowed aElement)
+{
   const nsMappedAttributes* attrs = aElement->GetMappedAttributes();
   if (!attrs) {
     auto* svg = nsSVGElement::FromContentOrNull(aElement);
     if (svg) {
       if (auto decl = svg->GetContentDeclarationBlock()) {
         return decl->AsServo()->RefRawStrong();
       }
     }
     return nullptr;
   }
 
-  const RefPtr<RawServoDeclarationBlock>& servo = attrs->GetServoStyle();
-  return reinterpret_cast<const RawServoDeclarationBlockStrong*>(&servo);
+  return AsRefRawStrong(attrs->GetServoStyle());
 }
 
 RawServoDeclarationBlockStrongBorrowedOrNull
 Gecko_GetExtraContentStyleDeclarations(RawGeckoElementBorrowed aElement)
 {
-  static_assert(sizeof(RefPtr<RawServoDeclarationBlock>) ==
-                sizeof(RawServoDeclarationBlockStrong),
-                "RefPtr should just be a pointer");
   if (!aElement->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th)) {
     return nullptr;
   }
   const HTMLTableCellElement* cell = static_cast<const HTMLTableCellElement*>(aElement);
   if (nsMappedAttributes* attrs = cell->GetMappedAttributesInheritedFromTable()) {
-    const RefPtr<RawServoDeclarationBlock>& servo = attrs->GetServoStyle();
-    return reinterpret_cast<const RawServoDeclarationBlockStrong*>(&servo);
+    return AsRefRawStrong(attrs->GetServoStyle());
   }
   return nullptr;
 }
 
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetUnvisitedLinkAttrDeclarationBlock(RawGeckoElementBorrowed aElement)
+{
+  nsHTMLStyleSheet* sheet = aElement->OwnerDoc()->GetAttributeStyleSheet();
+  if (!sheet) {
+    return nullptr;
+  }
+
+  return AsRefRawStrong(sheet->GetServoUnvisitedLinkDecl());
+}
+
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetVisitedLinkAttrDeclarationBlock(RawGeckoElementBorrowed aElement)
+{
+  nsHTMLStyleSheet* sheet = aElement->OwnerDoc()->GetAttributeStyleSheet();
+  if (!sheet) {
+    return nullptr;
+  }
+
+  return AsRefRawStrong(sheet->GetServoVisitedLinkDecl());
+}
+
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetActiveLinkAttrDeclarationBlock(RawGeckoElementBorrowed aElement)
+{
+  nsHTMLStyleSheet* sheet = aElement->OwnerDoc()->GetAttributeStyleSheet();
+  if (!sheet) {
+    return nullptr;
+  }
+
+  return AsRefRawStrong(sheet->GetServoActiveLinkDecl());
+}
+
 static nsIAtom*
 PseudoTagAndCorrectElementForAnimation(const Element*& aElementOrPseudo) {
   if (aElementOrPseudo->IsGeneratedContentContainerForBefore()) {
     aElementOrPseudo = aElementOrPseudo->GetParent()->AsElement();
     return nsCSSPseudoElements::before;
   }
 
   if (aElementOrPseudo->IsGeneratedContentContainerForAfter()) {
--- a/layout/style/ServoBindings.h
+++ b/layout/style/ServoBindings.h
@@ -192,16 +192,22 @@ SERVO_DECLARE_ELEMENT_ATTR_MATCHING_FUNC
 // Style attributes.
 RawServoDeclarationBlockStrongBorrowedOrNull
 Gecko_GetStyleAttrDeclarationBlock(RawGeckoElementBorrowed element);
 void Gecko_UnsetDirtyStyleAttr(RawGeckoElementBorrowed element);
 RawServoDeclarationBlockStrongBorrowedOrNull
 Gecko_GetHTMLPresentationAttrDeclarationBlock(RawGeckoElementBorrowed element);
 RawServoDeclarationBlockStrongBorrowedOrNull
 Gecko_GetExtraContentStyleDeclarations(RawGeckoElementBorrowed element);
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetUnvisitedLinkAttrDeclarationBlock(RawGeckoElementBorrowed element);
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetVisitedLinkAttrDeclarationBlock(RawGeckoElementBorrowed element);
+RawServoDeclarationBlockStrongBorrowedOrNull
+Gecko_GetActiveLinkAttrDeclarationBlock(RawGeckoElementBorrowed element);
 
 // Animations
 bool
 Gecko_GetAnimationRule(RawGeckoElementBorrowed aElementOrPseudo,
                        mozilla::EffectCompositor::CascadeLevel aCascadeLevel,
                        RawServoAnimationValueMapBorrowedMut aAnimationValues);
 RawServoDeclarationBlockStrongBorrowedOrNull
 Gecko_GetSMILOverrideDeclarationBlock(RawGeckoElementBorrowed element);
--- a/layout/style/StyleSheet.cpp
+++ b/layout/style/StyleSheet.cpp
@@ -604,18 +604,20 @@ StyleSheet::SubjectSubsumesInnerPrincipa
                                           ErrorResult& aRv)
 {
   StyleSheetInfo& info = SheetInfo();
 
   if (aSubjectPrincipal.Subsumes(info.mPrincipal)) {
     return;
   }
 
-  // Allow access only if CORS mode is not NONE
-  if (GetCORSMode() == CORS_NONE) {
+  // Allow access only if CORS mode is not NONE and the security flag
+  // is not turned off.
+  if (GetCORSMode() == CORS_NONE &&
+      !nsContentUtils::BypassCSSOMOriginCheck()) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return;
   }
 
   // Now make sure we set the principal of our inner to the subjectPrincipal.
   // We do this because we're in a situation where the caller would not normally
   // be able to access the sheet, but the sheet has opted in to being read.
   // Unfortunately, that means it's also opted in to being _edited_, and if the
--- a/layout/style/nsHTMLStyleSheet.cpp
+++ b/layout/style/nsHTMLStyleSheet.cpp
@@ -1,15 +1,15 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  *
- * This Original Code has been modified by IBM Corporation. Modifications made by IBM 
+ * This Original Code has been modified by IBM Corporation. Modifications made by IBM
  * described herein are Copyright (c) International Business Machines Corporation, 2000.
  * Modifications to Mozilla code or documentation identified per MPL Section 3.3
  *
  * Date             Modified by     Description of modification
  * 04/20/2000       IBM Corp.      OS/2 VisualAge build.
  */
 
 /*
@@ -31,16 +31,17 @@
 #include "nsRuleProcessorData.h"
 #include "nsCSSRuleProcessor.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/Element.h"
 #include "nsHashKeys.h"
 #include "mozilla/OperatorNewExtensions.h"
 #include "mozilla/RestyleManager.h"
 #include "mozilla/RestyleManagerInlines.h"
+#include "mozilla/ServoStyleSet.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 NS_IMPL_ISUPPORTS(nsHTMLStyleSheet::HTMLColorRule, nsIStyleRule)
 
 /* virtual */ void
 nsHTMLStyleSheet::HTMLColorRule::MapRuleInfoInto(nsRuleData* aRuleData)
@@ -471,61 +472,80 @@ nsHTMLStyleSheet::SetOwningDocument(nsID
 
 void
 nsHTMLStyleSheet::Reset()
 {
   mLinkRule          = nullptr;
   mVisitedRule       = nullptr;
   mActiveRule        = nullptr;
 
+  mServoUnvisitedLinkDecl = nullptr;
+  mServoVisitedLinkDecl = nullptr;
+  mServoActiveLinkDecl = nullptr;
+
   mLangRuleTable.Clear();
   mMappedAttrTable.Clear();
   mMappedAttrsDirty = false;
 }
 
 nsresult
-nsHTMLStyleSheet::ImplLinkColorSetter(RefPtr<HTMLColorRule>& aRule, nscolor aColor)
+nsHTMLStyleSheet::ImplLinkColorSetter(
+    RefPtr<HTMLColorRule>& aRule,
+    RefPtr<RawServoDeclarationBlock>& aDecl,
+    nscolor aColor)
 {
-  if (aRule && aRule->mColor == aColor) {
+  if (!mDocument || !mDocument->GetShell()) {
     return NS_OK;
   }
 
-  aRule = new HTMLColorRule(aColor);
-  if (!aRule)
-    return NS_ERROR_OUT_OF_MEMORY;
+  RestyleManager* restyle =
+    mDocument->GetShell()->GetPresContext()->RestyleManager();
+
+  if (restyle->IsServo()) {
+    MOZ_ASSERT(!ServoStyleSet::IsInServoTraversal());
+    aDecl = Servo_DeclarationBlock_CreateEmpty().Consume();
+    Servo_DeclarationBlock_SetColorValue(aDecl.get(), eCSSProperty_color,
+                                         aColor);
+  } else {
+    if (aRule && aRule->mColor == aColor) {
+      return NS_OK;
+    }
+
+    aRule = new HTMLColorRule(aColor);
+    if (!aRule) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+  }
 
   // Now make sure we restyle any links that might need it.  This
   // shouldn't happen often, so just rebuilding everything is ok.
-  if (mDocument && mDocument->GetShell()) {
-    Element* root = mDocument->GetRootElement();
-    if (root) {
-      mDocument->GetShell()->GetPresContext()->RestyleManager()->
-        PostRestyleEvent(root, eRestyle_Subtree, nsChangeHint(0));
-    }
+  Element* root = mDocument->GetRootElement();
+  if (root) {
+    restyle->PostRestyleEvent(root, eRestyle_Subtree, nsChangeHint(0));
   }
   return NS_OK;
 }
 
 nsresult
 nsHTMLStyleSheet::SetLinkColor(nscolor aColor)
 {
-  return ImplLinkColorSetter(mLinkRule, aColor);
+  return ImplLinkColorSetter(mLinkRule, mServoUnvisitedLinkDecl, aColor);
 }
 
 
 nsresult
 nsHTMLStyleSheet::SetActiveLinkColor(nscolor aColor)
 {
-  return ImplLinkColorSetter(mActiveRule, aColor);
+  return ImplLinkColorSetter(mActiveRule, mServoActiveLinkDecl, aColor);
 }
 
 nsresult
 nsHTMLStyleSheet::SetVisitedLinkColor(nscolor aColor)
 {
-  return ImplLinkColorSetter(mVisitedRule, aColor);
+  return ImplLinkColorSetter(mVisitedRule, mServoVisitedLinkDecl, aColor);
 }
 
 already_AddRefed<nsMappedAttributes>
 nsHTMLStyleSheet::UniqueMappedAttributes(nsMappedAttributes* aMapped)
 {
   mMappedAttrsDirty = true;
   auto entry = static_cast<MappedAttrTableEntry*>
                           (mMappedAttrTable.Add(aMapped, fallible));
--- a/layout/style/nsHTMLStyleSheet.h
+++ b/layout/style/nsHTMLStyleSheet.h
@@ -18,16 +18,17 @@
 #include "nsIStyleRuleProcessor.h"
 #include "PLDHashTable.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/MemoryReporting.h"
 #include "nsString.h"
 
 class nsIDocument;
 class nsMappedAttributes;
+struct RawServoDeclarationBlock;
 
 class nsHTMLStyleSheet final : public nsIStyleRuleProcessor
 {
 public:
   explicit nsHTMLStyleSheet(nsIDocument* aDocument);
 
   void SetOwningDocument(nsIDocument* aDocument);
 
@@ -53,16 +54,26 @@ public:
     const MOZ_MUST_OVERRIDE override;
   size_t DOMSizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
   void Reset();
   nsresult SetLinkColor(nscolor aColor);
   nsresult SetActiveLinkColor(nscolor aColor);
   nsresult SetVisitedLinkColor(nscolor aColor);
 
+  const RefPtr<RawServoDeclarationBlock>& GetServoUnvisitedLinkDecl() const {
+    return mServoUnvisitedLinkDecl;
+  }
+  const RefPtr<RawServoDeclarationBlock>& GetServoVisitedLinkDecl() const {
+    return mServoVisitedLinkDecl;
+  }
+  const RefPtr<RawServoDeclarationBlock>& GetServoActiveLinkDecl() const {
+    return mServoActiveLinkDecl;
+  }
+
   // Mapped Attribute management methods
   already_AddRefed<nsMappedAttributes>
     UniqueMappedAttributes(nsMappedAttributes* aMapped);
   void DropMappedAttributes(nsMappedAttributes* aMapped);
   // For each mapped presentation attribute in the cache, resolve
   // the attached ServoDeclarationBlock by running the mapping
   // and converting the ruledata to Servo specified values.
   void CalculateMappedServoDeclarations(nsPresContext* aPresContext);
@@ -95,17 +106,19 @@ private:
   #ifdef DEBUG
     virtual void List(FILE* out = stdout, int32_t aIndent = 0) const override;
   #endif
 
     nscolor mColor;
   };
 
   // Implementation of SetLink/VisitedLink/ActiveLinkColor
-  nsresult ImplLinkColorSetter(RefPtr<HTMLColorRule>& aRule, nscolor aColor);
+  nsresult ImplLinkColorSetter(RefPtr<HTMLColorRule>& aRule,
+                               RefPtr<RawServoDeclarationBlock>& aDecl,
+                               nscolor aColor);
 
   class GenericTableRule;
   friend class GenericTableRule;
   class GenericTableRule : public nsIStyleRule {
   protected:
     virtual ~GenericTableRule() {}
   public:
     GenericTableRule() {}
@@ -172,16 +185,19 @@ public: // for mLangRuleTable structures
     nsString mLang;
   };
 
 private:
   nsIDocument*            mDocument;
   RefPtr<HTMLColorRule> mLinkRule;
   RefPtr<HTMLColorRule> mVisitedRule;
   RefPtr<HTMLColorRule> mActiveRule;
+  RefPtr<RawServoDeclarationBlock> mServoUnvisitedLinkDecl;
+  RefPtr<RawServoDeclarationBlock> mServoVisitedLinkDecl;
+  RefPtr<RawServoDeclarationBlock> mServoActiveLinkDecl;
   RefPtr<TableQuirkColorRule> mTableQuirkColorRule;
   RefPtr<TableTHRule>   mTableTHRule;
 
   PLDHashTable            mMappedAttrTable;
   // Whether or not the mapped attributes table
   // has been changed since the last call to
   // CalculateMappedServoDeclarations()
   bool                    mMappedAttrsDirty;
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -772,17 +772,16 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'background/common/log/writers/LevelFilteringLogWriter.java',
     'background/common/log/writers/LogWriter.java',
     'background/common/log/writers/PrintLogWriter.java',
     'background/common/log/writers/SimpleTagLogWriter.java',
     'background/common/log/writers/StringLogWriter.java',
     'background/common/log/writers/TagLogWriter.java',
     'background/common/log/writers/ThreadLocalTagLogWriter.java',
     'background/common/PrefsBranch.java',
-    'background/common/telemetry/TelemetryWrapper.java',
     'background/db/CursorDumper.java',
     'background/db/Tab.java',
     'background/fxa/FxAccount20CreateDelegate.java',
     'background/fxa/FxAccount20LoginDelegate.java',
     'background/fxa/FxAccountClient.java',
     'background/fxa/FxAccountClient20.java',
     'background/fxa/FxAccountClientException.java',
     'background/fxa/FxAccountRemoteError.java',
@@ -1078,17 +1077,19 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'sync/synchronizer/SessionNotBegunException.java',
     'sync/synchronizer/Synchronizer.java',
     'sync/synchronizer/SynchronizerDelegate.java',
     'sync/synchronizer/SynchronizerSession.java',
     'sync/synchronizer/SynchronizerSessionDelegate.java',
     'sync/synchronizer/UnbundleError.java',
     'sync/synchronizer/UnexpectedSessionException.java',
     'sync/SynchronizerConfiguration.java',
+    'sync/telemetry/TelemetryCollector.java',
     'sync/telemetry/TelemetryContract.java',
+    'sync/telemetry/TelemetryStageCollector.java',
     'sync/ThreadPool.java',
     'sync/UnexpectedJSONException.java',
     'sync/UnknownSynchronizerConfigurationVersionException.java',
     'sync/Utils.java',
     'tokenserver/TokenServerClient.java',
     'tokenserver/TokenServerClientDelegate.java',
     'tokenserver/TokenServerException.java',
     'tokenserver/TokenServerToken.java',
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -38,23 +38,23 @@ import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mdns.MulticastDNSManager;
 import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.media.RemoteManager;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
 import org.mozilla.gecko.util.ActivityUtils;
+import org.mozilla.gecko.telemetry.TelemetryBackgroundReceiver;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.PRNGFixes;
 import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UUIDUtil;
 
 import java.io.File;
 import java.lang.reflect.Method;
 import java.util.UUID;
 
 public class GeckoApplication extends Application {
     private static final String LOG_TAG = "GeckoApplication";
     private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
@@ -261,16 +261,18 @@ public class GeckoApplication extends Ap
 
         HardwareUtils.init(context);
         FilePicker.init(context);
         DownloadsIntegration.init();
         HomePanelsManager.getInstance().init(context);
 
         GlobalPageMetadata.getInstance().init();
 
+        TelemetryBackgroundReceiver.getInstance().init(context);
+
         // We need to set the notification client before launching Gecko, since Gecko could start
         // sending notifications immediately after startup, which we don't want to lose/crash on.
         GeckoAppShell.setNotificationListener(new NotificationClient(context));
         // This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
         NotificationHelper.getInstance(context).init();
 
         MulticastDNSManager.getInstance(context).init();
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryBackgroundReceiver.java
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.telemetry;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetrySyncEventPingBuilder;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetrySyncPingBuilder;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetrySyncPingBundleBuilder;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
+import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Receives and processes telemetry broadcasts from background services, namely Sync.
+ * Nomenclature:
+ * - Bundled Sync Ping: a Sync Ping as documented at http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/data/sync-ping.html
+ *   as of commit https://github.com/mozilla-services/docs/commit/7eb4b412d3ab5ec46b280eff312ace32e7cf27e6
+ * - Telemetry data: incoming background telemetry, of two types: "sync" and "sync event"
+ * - Local Sync Ping: a persistable representation of incoming telemetry data. Not intended for upload.
+ *   See {@link TelemetryLocalPing}
+ *
+ * General flow:
+ * - background telemetry bundles come in, describing syncs or events that happened
+ * - telemetry bundles are transformed into a local pings and persisted
+ * - once there are enough local pings, or another upload condition kicks in, all of the persisted
+ *   local pings are bundled into a single outgoing Sync Ping, removed from the local store,
+ *   and Sync Ping is persisted and uploaded.
+ *
+ *   @author grisha
+ */
+public class TelemetryBackgroundReceiver extends BroadcastReceiver {
+    // NB: spelling is to appease logger's limitation on sizes of tags.
+    private final static String LOG_TAG = "TelemetryBgReceiver";
+
+    private static final String ACTION_BACKGROUND_TELEMETRY = "org.mozilla.gecko.telemetry.BACKGROUND";
+    private static final String SYNC_BUNDLE_STORE_DIR = "sync-ping-data";
+    private static final String SYNC_STORE_DIR = "sync-data";
+    private static final String SYNC_EVENT_STORE_DIR = "sync-event-data";
+    private static final int LOCAL_SYNC_EVENT_PING_THRESHOLD = 100;
+    private static final int LOCAL_SYNC_PING_THRESHOLD = 100;
+    private static final long MAX_TIME_BETWEEN_UPLOADS = 12 * 60 * 60 * 1000; // 12 hours
+
+    private static final String PREF_FILE_BACKGROUND_TELEMETRY = AppConstants.ANDROID_PACKAGE_NAME + ".telemetry.background";
+    private static final String PREF_IDS = "ids";
+    private static final String PREF_LAST_ATTEMPTED_UPLOADED = "last_attempted_upload";
+
+    // We don't currently support passing profile along with background telemetry. Profile is used to
+    // identify where pings are persisted locally.
+    private static final String DEFAULT_PROFILE = "default";
+
+    private static final TelemetryBackgroundReceiver instance = new TelemetryBackgroundReceiver();
+
+    public static TelemetryBackgroundReceiver getInstance() {
+        return instance;
+    }
+
+    public void init(Context context) {
+        LocalBroadcastManager.getInstance(context).registerReceiver(
+                this, new IntentFilter(ACTION_BACKGROUND_TELEMETRY));
+    }
+
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        Log.i(LOG_TAG, "Handling background telemetry broadcast");
+
+        if (!intent.hasExtra(TelemetryContract.KEY_TELEMETRY)) {
+            throw new IllegalStateException("Received a background telemetry broadcast without data.");
+        }
+
+        if (!intent.hasExtra(TelemetryContract.KEY_TYPE)) {
+            throw new IllegalStateException("Received a background telemetry broadcast without type.");
+        }
+
+        // We want to know if any of the below code is faulty in non-obvious ways, and as such there
+        // isn't an overarching try/catch to silence the errors.
+        // That is, let's crash here if something goes really wrong, and hope that we'll spot the
+        // error in the crash stats.
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                final String type = intent.getStringExtra(TelemetryContract.KEY_TYPE);
+
+                // Setup local telemetry stores.
+                final TelemetryJSONFilePingStore syncTelemetryStore = new TelemetryJSONFilePingStore(
+                        context.getFileStreamPath(SYNC_STORE_DIR), DEFAULT_PROFILE);
+                final TelemetryJSONFilePingStore syncEventTelemetryStore = new TelemetryJSONFilePingStore(
+                        context.getFileStreamPath(SYNC_EVENT_STORE_DIR), DEFAULT_PROFILE);
+
+                // Process incoming telemetry.
+                final Bundle telemetryBundle = intent.getParcelableExtra(TelemetryContract.KEY_TELEMETRY);
+                final String uid = telemetryBundle.getString(TelemetryContract.KEY_LOCAL_UID);
+                final String deviceID = telemetryBundle.getString(TelemetryContract.KEY_LOCAL_DEVICE_ID);
+
+                // Transform incoming telemetry into a local ping of correct type (sync vs event).
+                final TelemetryLocalPing localPing;
+                final TelemetryPingStore telemetryStore;
+                switch (type) {
+                    case TelemetryContract.KEY_TYPE_SYNC:
+                        final ArrayList<Parcelable> devices = telemetryBundle.getParcelableArrayList(TelemetryContract.KEY_DEVICES);
+                        final Serializable error = telemetryBundle.getSerializable(TelemetryContract.KEY_ERROR);
+                        final Serializable stages = telemetryBundle.getSerializable(TelemetryContract.KEY_STAGES);
+                        final long took = telemetryBundle.getLong(TelemetryContract.KEY_TOOK);
+                        final boolean didRestart = telemetryBundle.getBoolean(TelemetryContract.KEY_RESTARTED);
+
+                        telemetryStore = syncTelemetryStore;
+                        TelemetrySyncPingBuilder localPingBuilder = new TelemetrySyncPingBuilder();
+
+                        if (uid != null) {
+                            localPingBuilder.setUID(uid);
+                        }
+
+                        if (deviceID != null) {
+                            localPingBuilder.setDeviceID(deviceID);
+                        }
+
+                        if (devices != null) {
+                            localPingBuilder.setDevices(devices);
+                        }
+
+                        if (stages != null) {
+                            localPingBuilder.setStages(stages);
+                        }
+
+                        if (error != null) {
+                            localPingBuilder.setError(error);
+                        }
+
+                        localPing = localPingBuilder
+                                .setRestarted(didRestart)
+                                .setTook(took)
+                                .build();
+                        break;
+                    case TelemetryContract.KEY_TYPE_EVENT:
+                        telemetryStore = syncEventTelemetryStore;
+                        localPing = new TelemetrySyncEventPingBuilder()
+                                .fromEventTelemetry(
+                                        (Bundle) intent.getParcelableExtra(
+                                                TelemetryContract.KEY_TELEMETRY))
+                                .build();
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unknown background telemetry type.");
+                }
+
+                // Persist the incoming telemetry data.
+                try {
+                    telemetryStore.storePing(localPing);
+                } catch (IOException e) {
+                    Log.e(LOG_TAG, "Could not store incoming telemetry. Attempting to upload already stored telemetry.", e);
+                }
+
+                // Determine if we should try uploading at this point, and attempt to do so.
+                final SharedPreferences sharedPreferences = context.getSharedPreferences(
+                        PREF_FILE_BACKGROUND_TELEMETRY, Context.MODE_PRIVATE);
+                final TelemetryPingStore syncPingStore = new TelemetryJSONFilePingStore(
+                        context.getFileStreamPath(SYNC_BUNDLE_STORE_DIR), DEFAULT_PROFILE);
+
+                long lastAttemptedSyncPingUpload = sharedPreferences.getLong(PREF_LAST_ATTEMPTED_UPLOADED, 0L);
+                boolean idsChanged = setOrUpdateIDsIfChanged(sharedPreferences, uid, deviceID);
+
+                // Is there a good reason to upload at this time?
+                final String reasonToUpload = reasonToUpload(
+                        idsChanged,
+                        syncTelemetryStore.getCount(),
+                        syncEventTelemetryStore.getCount(),
+                        lastAttemptedSyncPingUpload
+                );
+
+                // If we have a reason to upload at this point, bundle up sync and event telemetry
+                // into a Sync Ping.
+                if (reasonToUpload != null) {
+                    // Get the IDs of telemetry objects we're about to bundle up.
+                    // Note that this may race with other incoming background telemetry.
+                    // We may accidentally drop non-bundled local telemetry if it was emitted while
+                    // we're processing the current telemetry.
+                    // Chances of that happening are very small due to how often background telemetry
+                    // is actually emitted: very infrequently on a timescale of disk access.
+                    final Set<String> localSyncTelemetryToRemove = syncTelemetryStore.getStoredIDs();
+                    final Set<String> localSyncEventTelemetryToRemove = syncEventTelemetryStore.getStoredIDs();
+
+                    // Bundle up all that we have in our telemetry stores.
+                    final TelemetryOutgoingPing syncPing = new TelemetrySyncPingBundleBuilder()
+                            .setSyncStore(syncTelemetryStore)
+                            .setSyncEventStore(syncEventTelemetryStore)
+                            .setReason(reasonToUpload)
+                            .build();
+
+                    // Persist a Sync Ping Bundle.
+                    boolean bundledSyncPingPersisted = true;
+                    try {
+                        syncPingStore.storePing(syncPing);
+                    } catch (IOException e) {
+                        // If we fail to persist a bundled sync ping, we can either attempt to upload it,
+                        // or skip the upload. Each choice has its own set of trade-offs.
+                        // In short, current approach is to skip an upload. See Bug 1369186.
+                        //
+                        // If we choose to upload a Sync Ping that failed to persist locally, it becomes
+                        // possible to upload the same telemetry multiple times. Since currently we do
+                        // not have IDs as part of our sync and event telemetry objects, it is impossible
+                        // to guarantee idempotence on the receiver's end. As such, we care to not
+                        // upload the same thing multiple times. One way to achieve this involves
+                        // creating an additional mapping of Bundled Sync Ping ID to two sets,
+                        // {sync telemetry ids} and {event telemetry ids}, and taking care to only include
+                        // telemetry in the subsequent pings which has not yet been associated with a
+                        // Sync Ping Bundle. Given the fact that this will likely use a SharedPreference
+                        // and that we inherently have a concurrent telemetry pipeline, it's quite possible
+                        // that we'll get this wrong in some subtle way after considerable effort.
+                        //
+                        // An alternative is to simply not upload if we failed to persist a Sync Ping.
+                        // This side-steps issues of idempotancy, but comes at a risk of taking longer
+                        // to upload telemetry, or sometimes not uploading it at all - see Bug 1366045.
+                        // However, those issues become likely only if our local JSON storage is failing
+                        // frequently. This should not happen under most circumstances, and if it does
+                        // happen frequently, we're likely to have a host of other problems.
+                        //
+                        // Yet another solution is to alter server-side processing of this data such that
+                        // a unique ID may be included alongside each sync/event telemetry object. This
+                        // will result in a very straightforward client implementation with much better
+                        // consistency guarantees.
+                        //
+                        // See Bug 1368579 for exploring possible "runaway storage" implications of
+                        // the current approach.
+                        //
+                        // Also note that a core assumption here is that storePing never successfully writes
+                        // to disk if it throws.
+                        Log.e(LOG_TAG, "Unable to write bundled sync ping to disk. Skipping upload.", e);
+                        bundledSyncPingPersisted = false;
+                    }
+
+                    if (bundledSyncPingPersisted) {
+                        // It is now safe to delete persisted telemetry which we just bundled up.
+                        syncTelemetryStore.onUploadAttemptComplete(localSyncTelemetryToRemove);
+                        syncEventTelemetryStore.onUploadAttemptComplete(localSyncEventTelemetryToRemove);
+                    }
+                }
+
+                // Kick-off ping upload. If this succeeds, the uploader service will remove persisted
+                // Sync Ping Bundles. Otherwise, we'll attempt another upload next time telemetry is
+                // processed.
+                // If we already have some persisted Sync Pings, that means a previous upload
+                // failed - or, less likely, is in progress and did not yet succeed. It should be safe to
+                // upload. Even if we raced with ourselves and uploaded some of the bundled sync pings more
+                // than once, it's possible to guarantee idempotence on the receiver's end since we
+                // include a unique ID with each ping. However, this depends on the telemetry pipeline
+                // successfully de-duplicating submitted pings. As of Q2 2017, success rate of de-duping
+                // sits around 45%, and is anticipated to be improved to 80-90% sometime by the end of 2017.
+                // Relevant bugs are 1369512, 1357275.
+                // Not uploading here means possibly delaying an already once-failed upload for a long
+                // time. See Bug 1366045 for exploring scheduling options.
+                if (reasonToUpload != null || syncPingStore.getCount() > 0) {
+                    // Bump the "last-attempted-uploaded" timestamp, even though we might still fail
+                    // to upload. Since we check for presence of pending pings above, if this upload
+                    // fails we'll try again whenever next telemetry event happens.
+                    sharedPreferences
+                            .edit()
+                            .putLong(PREF_LAST_ATTEMPTED_UPLOADED, System.currentTimeMillis())
+                            .apply();
+
+                    final TelemetryUploadScheduler scheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
+                    if (scheduler.isReadyToUpload(context, syncPingStore)) {
+                        scheduler.scheduleUpload(context, syncPingStore);
+                    }
+                }
+            }
+        });
+    }
+
+    // There's no "scheduler" in a classic sense, and so we might end up not uploading pings at all
+    // if there has been no new incoming telemetry data. See Bug 1366045.
+    @Nullable
+    protected static String reasonToUpload(boolean idsChanged, int syncCount, int eventCount, long lastUploadAttempt) {
+        // Whenever any IDs change, upload.
+        if (idsChanged) {
+            return TelemetrySyncPingBundleBuilder.UPLOAD_REASON_IDCHANGE;
+        }
+
+        // Whenever we hit a certain threshold of local persisted telemetry, upload.
+        if (syncCount > LOCAL_SYNC_PING_THRESHOLD || eventCount > LOCAL_SYNC_EVENT_PING_THRESHOLD) {
+            return TelemetrySyncPingBundleBuilder.UPLOAD_REASON_COUNT;
+        }
+
+        final long now = System.currentTimeMillis();
+
+        // If it's the first time we're processing telemetry data, upload ahead of schedule as a way
+        // of saying "we're alive". This might often correspond to sending data about the first sync.
+        if (lastUploadAttempt == 0L) {
+            return TelemetrySyncPingBundleBuilder.UPLOAD_REASON_FIRST;
+        }
+
+        // Wall clock changed significantly; upload because we can't be sure of our timing anymore.
+        // Allow for some wiggle room to account for clocks jumping around insignificantly.
+        final long DRIFT_BUFFER_IN_MS = 60 * 1000L;
+        if ((lastUploadAttempt - now) > DRIFT_BUFFER_IN_MS) {
+            return TelemetrySyncPingBundleBuilder.UPLOAD_REASON_CLOCK_DRIFT;
+        }
+
+        // Upload if we haven't uploaded for some time.
+        if ((now - lastUploadAttempt) >= MAX_TIME_BETWEEN_UPLOADS) {
+            return TelemetrySyncPingBundleBuilder.UPLOAD_REASON_SCHEDULE;
+        }
+
+        // No reason to upload.
+        return null;
+    }
+
+    // This has storage side-effects.
+    private boolean setOrUpdateIDsIfChanged(SharedPreferences prefs, String uid, String deviceID) {
+        final String currentIDsCombined = uid.concat(deviceID);
+        final String previousIDsHash = prefs.getString(PREF_IDS, "");
+
+        // Persist IDs for the first time, declare them as "not changed".
+        if (previousIDsHash.equals("")) {
+            final SharedPreferences.Editor prefsEditor = prefs.edit();
+            prefsEditor.putString(PREF_IDS, currentIDsCombined);
+            prefsEditor.apply();
+            return false;
+        }
+
+        // If IDs are different update local cache and declare them as "changed".
+        if (!previousIDsHash.equals(currentIDsCombined)) {
+            final SharedPreferences.Editor prefsEditor = prefs.edit();
+            prefsEditor.putString(PREF_IDS, currentIDsCombined);
+            prefsEditor.apply();
+            return true;
+        }
+
+        // Nothing changed, and no side-effects took place.
+        return false;
+    }
+
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
@@ -67,52 +67,52 @@ public class TelemetryDispatcher {
         // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated
         // when the ping is stored. However, for simplicity, we use the json store and accept the possible
         // loss of data (see bug 1243585 comment 16+ for more).
         coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME), profileName);
 
         uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
     }
 
-    private void queuePingForUpload(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
-            final TelemetryUploadScheduler scheduler) {
+    private void queuePingForUpload(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store,
+                                    final TelemetryUploadScheduler scheduler) {
         final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler);
         ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another.
     }
 
     /**
      * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
      */
     public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
-        final TelemetryPing ping = pingBuilder.build();
+        final TelemetryOutgoingPing ping = pingBuilder.build();
         queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
     }
 
-    private static class QueuePingRunnable implements Runnable {
+    /* package-private */ static class QueuePingRunnable implements Runnable {
         private final Context applicationContext;
-        private final TelemetryPing ping;
+        private final TelemetryOutgoingPing ping;
         private final TelemetryPingStore store;
         private final TelemetryUploadScheduler scheduler;
 
-        public QueuePingRunnable(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
-                final TelemetryUploadScheduler scheduler) {
+        /* package-private */ QueuePingRunnable(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store,
+                                                final TelemetryUploadScheduler scheduler) {
             this.applicationContext = context.getApplicationContext();
             this.ping = ping;
             this.store = store;
             this.scheduler = scheduler;
         }
 
         @Override
         public void run() {
             // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value.
             try {
                 store.storePing(ping);
             } catch (final IOException e) {
                 // Don't log exception to avoid leaking profile path.
                 Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt");
             }
 
-            if (scheduler.isReadyToUpload(store)) {
+            if (scheduler.isReadyToUpload(applicationContext, store)) {
                 scheduler.scheduleUpload(applicationContext, store);
             }
         }
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryLocalPing.java
@@ -0,0 +1,34 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.telemetry;
+
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * A "local" ping, which is not intended to be uploaded, but simply stored for later processing.
+ * Currently, many instances of local pings are bundled into a Sync Ping at appropriate moments.
+ */
+public class TelemetryLocalPing implements TelemetryPing {
+    private final ExtendedJSONObject payload;
+    private final String docID;
+
+    public TelemetryLocalPing(final ExtendedJSONObject payload, final String docID) {
+        this.payload = payload;
+        this.docID = docID;
+    }
+
+    public ExtendedJSONObject getPayload() { return payload; }
+    public String getDocID() { return docID; }
+
+    // Following the path of least resistance to avoid decoupling a ping from where it should
+    // be uploaded, for a local ping we declare that the path is nullable, and in fact it's always null.
+    @Nullable
+    public String getURLPath() {
+        return null;
+    }
+}
copy from mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
copy to mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryOutgoingPing.java
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryOutgoingPing.java
@@ -12,23 +12,31 @@ import org.mozilla.gecko.sync.ExtendedJS
  *
  * The doc ID is used by a Store to manipulate its internal pings and should
  * be the same value found in the urlPath.
  *
  * If you want to create one of these, consider extending
  * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder}
  * or one of its descendants.
  */
-public class TelemetryPing {
-    private final String urlPath;
+public class TelemetryOutgoingPing implements TelemetryPing {
     private final ExtendedJSONObject payload;
     private final String docID;
+    private final String urlPath;
 
-    public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final String docID) {
-        this.urlPath = urlPath;
+    public TelemetryOutgoingPing(final String urlPath, final ExtendedJSONObject payload, final String docID) {
         this.payload = payload;
         this.docID = docID;
+        this.urlPath = urlPath;
     }
 
     public String getURLPath() { return urlPath; }
-    public ExtendedJSONObject getPayload() { return payload; }
-    public String getDocID() { return docID; }
+
+    @Override
+    public ExtendedJSONObject getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String getDocID() {
+        return docID;
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
@@ -2,33 +2,13 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.telemetry;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
-/**
- * Container for telemetry data and the data necessary to upload it.
- *
- * The doc ID is used by a Store to manipulate its internal pings and should
- * be the same value found in the urlPath.
- *
- * If you want to create one of these, consider extending
- * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder}
- * or one of its descendants.
- */
-public class TelemetryPing {
-    private final String urlPath;
-    private final ExtendedJSONObject payload;
-    private final String docID;
-
-    public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final String docID) {
-        this.urlPath = urlPath;
-        this.payload = payload;
-        this.docID = docID;
-    }
-
-    public String getURLPath() { return urlPath; }
-    public ExtendedJSONObject getPayload() { return payload; }
-    public String getDocID() { return docID; }
+public interface TelemetryPing {
+    ExtendedJSONObject getPayload();
+    String getDocID();
+    String getURLPath();
 }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -9,16 +9,17 @@ import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 import ch.boye.httpclientandroidlib.HttpHeaders;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
@@ -99,16 +100,19 @@ public class TelemetryUploadService exte
         if (pingsToUpload.isEmpty()) {
             return true;
         }
 
         final String serverSchemeHostPort = TelemetryPreferences.getServerSchemeHostPort(context, store.getProfileName());
         final HashSet<String> successfulUploadIDs = new HashSet<>(pingsToUpload.size()); // used for side effects.
         final PingResultDelegate delegate = new PingResultDelegate(successfulUploadIDs);
         for (final TelemetryPing ping : pingsToUpload) {
+            if (!(ping instanceof TelemetryOutgoingPing)) {
+                throw new IllegalStateException("Tried uploading a non-outgoing ping.");
+            }
             // TODO: It'd be great to re-use the same HTTP connection for each upload request.
             delegate.setDocID(ping.getDocID());
             final String url = serverSchemeHostPort + "/" + ping.getURLPath();
             uploadPayload(url, ping.getPayload(), delegate);
 
             // There are minimal gains in trying to upload if we already failed one attempt.
             if (delegate.hadConnectionError()) {
                 break;
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
@@ -10,36 +10,36 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Build;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 
 import android.util.Log;
-import org.mozilla.gecko.AppConstants;
+
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.search.SearchEngine;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.TelemetryPing;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
 import org.mozilla.gecko.util.DateUtil;
 import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Builds a {@link TelemetryPing} representing a core ping.
+ * Builds a {@link TelemetryOutgoingPing} representing a core ping.
  *
  * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
  * for details on the core ping.
  */
 public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
     private static final String LOGTAG = StringUtils.safeSubstring(TelemetryCorePingBuilder.class.getSimpleName(), 0, 23);
 
     // For legacy reasons, this preference key is not namespaced with "core".
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryLocalPingBuilder.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+
+import java.util.UUID;
+
+abstract class TelemetryLocalPingBuilder {
+    final ExtendedJSONObject payload = new ExtendedJSONObject();
+    final String docID = UUID.randomUUID().toString();
+
+    abstract TelemetryLocalPing build();
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
@@ -3,36 +3,36 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 package org.mozilla.gecko.telemetry.pingbuilders;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.TelemetryPing;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
 
 import java.util.Set;
 import java.util.UUID;
 
 /**
- * A generic Builder for {@link TelemetryPing} instances. Each overriding class is
+ * A generic Builder for {@link TelemetryOutgoingPing} instances. Each overriding class is
  * expected to create a specific type of ping (e.g. "core").
  *
  * This base class handles the common ping operations under the hood:
  *   * Validating mandatory fields
  *   * Forming the server url
  */
 abstract class TelemetryPingBuilder {
     // In the server url, the initial path directly after the "scheme://host:port/"
     private static final String SERVER_INITIAL_PATH = "submit/telemetry";
 
     private final String serverPath;
     protected final ExtendedJSONObject payload;
-    private final String docID;
+    protected final String docID;
 
     public TelemetryPingBuilder() {
         docID = UUID.randomUUID().toString();
         serverPath = getTelemetryServerPath(getDocType(), docID);
         payload = new ExtendedJSONObject();
     }
 
     /**
@@ -41,19 +41,19 @@ abstract class TelemetryPingBuilder {
     public abstract String getDocType();
 
     /**
      * @return the fields that are mandatory for the resultant ping to be uploaded to
      *         the server. These will be validated before the ping is built.
      */
     public abstract String[] getMandatoryFields();
 
-    public TelemetryPing build() {
+    public TelemetryOutgoingPing build() {
         validatePayload();
-        return new TelemetryPing(serverPath, payload, docID);
+        return new TelemetryOutgoingPing(serverPath, payload, docID);
     }
 
     private void validatePayload() {
         final Set<String> keySet = payload.keySet();
         for (final String mandatoryField : getMandatoryFields()) {
             if (!keySet.contains(mandatoryField)) {
                 throw new IllegalArgumentException("Builder does not contain mandatory field: " +
                         mandatoryField);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetrySyncEventPingBuilder.java
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.os.Bundle;
+
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+
+/**
+ * Local ping builder which understands how to process event data.
+ * This is a placeholder, to be implemented in Bug 1363924.
+ */
+public class TelemetrySyncEventPingBuilder extends TelemetryLocalPingBuilder {
+    public TelemetrySyncEventPingBuilder fromEventTelemetry(Bundle data) {
+        return this;
+    }
+
+    @Override
+    public TelemetryLocalPing build() {
+        throw new UnsupportedOperationException();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetrySyncPingBuilder.java
@@ -0,0 +1,166 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Local ping builder which understands how to process sync data.
+ * Whenever hashing of data is involved, we expect it to be performed at the time of collection,
+ * somewhere in {@link org.mozilla.gecko.sync.telemetry.TelemetryCollector} and friends.
+ */
+public class TelemetrySyncPingBuilder extends TelemetryLocalPingBuilder {
+    private static final int DATA_FORMAT_VERSION = 1;
+
+    public TelemetrySyncPingBuilder setStages(@NonNull final Serializable data) {
+        HashMap<String, TelemetryStageCollector> stages = castSyncData(data);
+
+        final JSONArray engines = new JSONArray();
+        for (String stageName : stages.keySet()) {
+            final TelemetryStageCollector stage = stages.get(stageName);
+
+            // Skip stages that did nothing.
+            if (stage.inbound == 0 && stage.outbound == 0) {
+                continue;
+            }
+
+            final ExtendedJSONObject stageJSON = new ExtendedJSONObject();
+
+            stageJSON.put("name", stageName);
+            stageJSON.put("took", stage.finished - stage.started);
+
+            // Desktop also includes a "status" field with internal constants as possible values.
+            // Status may be deducted by inspecting 'failureReason', and as such it is omitted here.
+            // Absence of 'failureReason' means that stage succeeded.
+
+            if (stage.inbound > 0) {
+                final ExtendedJSONObject incomingJSON = new ExtendedJSONObject();
+                incomingJSON.put("applied", stage.inbound);
+                if (stage.inboundStored > 0) {
+                    incomingJSON.put("succeeded", stage.inboundStored);
+                }
+                if (stage.inboundFailed > 0) {
+                    incomingJSON.put("failed", stage.inboundFailed);
+                }
+                if (stage.reconciled > 0) {
+                    incomingJSON.put("reconciled", stage.reconciled);
+                }
+                stageJSON.put("incoming", incomingJSON);
+            }
+
+            if (stage.outbound > 0) {
+                final ExtendedJSONObject outgoingJSON = new ExtendedJSONObject();
+                // We specifically do not check if `outboundStored` is greater than zero.
+                // `outbound` schema is simpler than `inbound`, namely there isn't an "attempted
+                // to send" count.
+                // Stage telemetry itself has that data (outbound = outboundStored + outboundFailed),
+                // and so this is our way to relay slightly more information.
+                // e.g. we'll know there's something wrong if `sent = 0` and `failed` is missing.
+                outgoingJSON.put("sent", stage.outboundStored);
+                if (stage.outboundFailed > 0) {
+                    outgoingJSON.put("failed", stage.outboundFailed);
+                }
+                stageJSON.put("outgoing", outgoingJSON);
+            }
+
+            // We depend on the error builder from TelemetryCollector to produce the right schema.
+            // Spreading around our schema definition like that is awkward, but, alas, here we are.
+            if (stage.error != null) {
+                stageJSON.put("failureReason", stage.error);
+            }
+
+            addUnchecked(engines, stageJSON);
+        }
+        payload.put("engines", engines);
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setUID(@NonNull String uid) {
+        payload.put("uid", uid);
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setDeviceID(@NonNull String deviceID) {
+        payload.put("deviceID", deviceID);
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setRestarted(boolean didRestart) {
+        if (!didRestart) {
+            return this;
+        }
+
+        payload.put("restarted", true);
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setDevices(ArrayList<Parcelable> devices) {
+        final JSONArray devicesJSON = new JSONArray();
+
+        for (Parcelable device : devices) {
+            final Bundle deviceBundle = (Bundle) device;
+            final ExtendedJSONObject deviceJSON = new ExtendedJSONObject();
+
+            deviceJSON.put("os", deviceBundle.getString(TelemetryContract.KEY_DEVICE_OS));
+            deviceJSON.put("version", deviceBundle.getString(TelemetryContract.KEY_DEVICE_VERSION));
+            deviceJSON.put("id", deviceBundle.getString(TelemetryContract.KEY_DEVICE_ID));
+
+            addUnchecked(devicesJSON, deviceJSON);
+        }
+
+        if (devicesJSON.size() > 0) {
+            payload.put("devices", devicesJSON);
+        }
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setError(@NonNull Serializable error) {
+        payload.put("failureReason", castErrorObject(error));
+        return this;
+    }
+
+    public TelemetrySyncPingBuilder setTook(long took) {
+        payload.put("took", took);
+        return this;
+    }
+
+    @Override
+    public TelemetryLocalPing build() {
+        payload.put("version", DATA_FORMAT_VERSION);
+        return new TelemetryLocalPing(payload, docID);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void addUnchecked(final JSONArray list, final ExtendedJSONObject obj) {
+        list.add(obj);
+    }
+
+    /**
+     * We broadcast this data via LocalBroadcastManager and control both sides of this code, so it
+     * is acceptable to do an unchecked cast.
+     */
+    @SuppressWarnings("unchecked")
+    private static HashMap<String, TelemetryStageCollector> castSyncData(final Serializable data) {
+        return (HashMap<String, TelemetryStageCollector>) data;
+    }
+
+    private static ExtendedJSONObject castErrorObject(final Serializable error) {
+        return (ExtendedJSONObject) error;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetrySyncPingBundleBuilder.java
@@ -0,0 +1,114 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.os.Build;
+import android.support.annotation.NonNull;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
+import org.mozilla.gecko.telemetry.TelemetryPing;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Responsible for building a Sync Ping, based on the telemetry docs:
+ * http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/data/sync-ping.html
+ *
+ * This builder takes two stores ('sync' and 'event') and produces a single "sync ping".
+ *
+ * Note that until Bug 1363924, event telemetry will be ignored.
+ *
+ * Sample result will look something like:
+ * {
+ *     "syncs": [list of syncs, as produced by the SyncBuilder],
+ *     "events": [list of events, as produced by the EventBuilder]
+ * }
+ */
+public class TelemetrySyncPingBundleBuilder extends TelemetryPingBuilder {
+    private static final String PING_TYPE = "sync";
+    private static final int PING_VERSION = 4;
+
+    public static final String UPLOAD_REASON_FIRST = "first";
+    public static final String UPLOAD_REASON_CLOCK_DRIFT = "clockdrift";
+    public static final String UPLOAD_REASON_SCHEDULE = "schedule";
+    public static final String UPLOAD_REASON_IDCHANGE = "idchange";
+    public static final String UPLOAD_REASON_COUNT = "count";
+
+    private final ExtendedJSONObject pingData = new ExtendedJSONObject();
+
+    @Override
+    public String getDocType() {
+        return "sync";
+    }
+
+    @Override
+    public String[] getMandatoryFields() {
+        return new String[0];
+    }
+
+    public TelemetrySyncPingBundleBuilder setReason(@NonNull String reason) {
+        pingData.put("why", reason);
+        return this;
+    }
+
+    @Override
+    public TelemetryOutgoingPing build() {
+        final DateFormat pingCreationDateFormat = new SimpleDateFormat(
+                "yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+        pingCreationDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        payload.put("type", PING_TYPE);
+        payload.put("version", PING_VERSION);
+        payload.put("id", docID);
+        payload.put("creationDate", pingCreationDateFormat.format(new Date()));
+
+        final ExtendedJSONObject application = new ExtendedJSONObject();
+        application.put("architecture", Build.CPU_ABI);
+        application.put("buildID", AppConstants.MOZ_APP_BUILDID);
+        application.put("platformVersion", AppConstants.MOZ_APP_VERSION);
+        application.put("name", AppConstants.MOZ_APP_BASENAME);
+        application.put("version", AppConstants.MOZ_APP_VERSION);
+        application.put("displayVersion", AppConstants.MOZ_APP_VERSION);
+        application.put("vendor", AppConstants.MOZ_APP_VENDOR);
+        application.put("xpcomAbi", AppConstants.MOZ_APP_ABI);
+        application.put("channel", AppConstants.MOZ_UPDATE_CHANNEL);
+
+        payload.put("application", application);
+        payload.put("payload", pingData);
+        return super.build();
+    }
+
+    @SuppressWarnings("unchecked")
+    public TelemetrySyncPingBundleBuilder setSyncStore(TelemetryPingStore store) {
+        final JSONArray syncs = new JSONArray();
+        List<TelemetryPing> pings = store.getAllPings();
+
+        // Please note how we're not including constituent ping's docID in the final payload. This is
+        // unfortunate and causes some grief when managing local ping storage and uploads, but needs
+        // to be resolved beyond this individual client. See Bug 1369186.
+        for (TelemetryPing ping : pings) {
+            syncs.add(ping.getPayload());
+        }
+
+        pingData.put("syncs", syncs);
+        return this;
+    }
+
+    // Event telemetry will be implemented in Bug 1363924.
+    public TelemetrySyncPingBundleBuilder setSyncEventStore(TelemetryPingStore store) {
+        return this;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java
@@ -12,17 +12,17 @@ import org.mozilla.gecko.telemetry.store
 import org.mozilla.gecko.telemetry.TelemetryUploadService;
 
 /**
  * Schedules an upload with all pings to be sent immediately.
  */
 public class TelemetryUploadAllPingsImmediatelyScheduler implements TelemetryUploadScheduler {
 
     @Override
-    public boolean isReadyToUpload(final TelemetryPingStore store) {
+    public boolean isReadyToUpload(final Context applicationContext, final TelemetryPingStore store) {
         // We're ready since we don't have any conditions to wait on (e.g. on wifi, accumulated X pings).
         return true;
     }
 
     @Override
     public void scheduleUpload(final Context applicationContext, final TelemetryPingStore store) {
         final Intent i = new Intent(TelemetryUploadService.ACTION_UPLOAD);
         i.setClass(applicationContext, TelemetryUploadService.class);
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java
@@ -16,11 +16,11 @@ import org.mozilla.gecko.telemetry.store
  * scheduled by sending an {@link android.content.Intent} to the
  * {@link org.mozilla.gecko.telemetry.TelemetryUploadService}, either immediately or
  * via an external scheduler (e.g. {@link android.app.job.JobScheduler}).
  *
  * N.B.: If the Store is not ready to upload, an implementation *should not* try to reschedule
  * the check to see if it's time to upload - this is expected to be handled by the caller.
  */
 public interface TelemetryUploadScheduler {
-    boolean isReadyToUpload(TelemetryPingStore store);
+    boolean isReadyToUpload(Context applicationContext, TelemetryPingStore store);
     void scheduleUpload(Context applicationContext, TelemetryPingStore store);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
@@ -3,23 +3,26 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, you can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 package org.mozilla.gecko.telemetry.stores;
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
 import org.mozilla.gecko.telemetry.TelemetryPing;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator;
 import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter;
 import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.UUIDUtil;
 
@@ -28,21 +31,20 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.nio.channels.FileLock;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
 
 /**
  * An implementation of TelemetryPingStore that is backed by JSON files.
  *
  * This implementation seeks simplicity. Each ping to upload is stored in its own file with its doc ID
  * as the filename. The doc ID is sent with a ping to be uploaded and is expected to be returned with
  * {@link #onUploadAttemptComplete(Set)} so the associated file can be removed.
  *
@@ -95,29 +97,34 @@ public class TelemetryJSONFilePingStore 
     }
 
     @VisibleForTesting File getPingFile(final String docID) {
         return new File(storeDir, docID);
     }
 
     @Override
     public void storePing(final TelemetryPing ping) throws IOException {
-        final String output;
+        storePing(ping.getPayload(), ping.getDocID(), ping.getURLPath());
+    }
+
+    public void storePing(final ExtendedJSONObject payload, String docID, @Nullable String urlPath) throws IOException {
+        final JSONObject output;
         try {
-            output = new JSONObject()
-                    .put(KEY_PAYLOAD, ping.getPayload())
-                    .put(KEY_URL_PATH, ping.getURLPath())
-                    .toString();
+            output = new JSONObject().put(KEY_PAYLOAD, payload);
+
+            if (urlPath != null) {
+                output.put(KEY_URL_PATH, urlPath);
+            }
         } catch (final JSONException e) {
             // Do not log the exception to avoid leaking personal data.
             throw new IOException("Unable to create JSON to store to disk");
         }
 
-        final FileOutputStream outputStream = new FileOutputStream(getPingFile(ping.getDocID()), false);
-        blockForLockAndWriteFileAndCloseStream(outputStream, output);
+        final FileOutputStream outputStream = new FileOutputStream(getPingFile(docID), false);
+        blockForLockAndWriteFileAndCloseStream(outputStream, output.toString());
     }
 
     @Override
     public void maybePrunePings() {
         final File[] files = storeDir.listFiles(uuidFilenameFilter);
         if (files == null) {
             return;
         }
@@ -162,28 +169,59 @@ public class TelemetryJSONFilePingStore 
         final ArrayList<TelemetryPing> out = new ArrayList<>(files.size());
         for (final File file : files) {
             final JSONObject obj = lockAndReadJSONFromFile(file);
             if (obj == null) {
                 // We log in the method to get the JSONObject if we return null.
                 continue;
             }
 
+            final ExtendedJSONObject payload;
             try {
-                final String url = obj.getString(KEY_URL_PATH);
-                final ExtendedJSONObject payload = new ExtendedJSONObject(obj.getString(KEY_PAYLOAD));
-                out.add(new TelemetryPing(url, payload, file.getName()));
-            } catch (final IOException | JSONException | NonObjectJSONException e) {
+                payload = new ExtendedJSONObject(obj.getString(KEY_PAYLOAD));
+            } catch (IOException | JSONException | NonObjectJSONException e) {
                 Log.w(LOGTAG, "Bad json in ping. Ignoring.");
                 continue;
             }
+
+            try {
+                final String url = obj.getString(KEY_URL_PATH);
+                out.add(new TelemetryOutgoingPing(url, payload, file.getName()));
+            } catch (JSONException e) {
+                out.add(new TelemetryLocalPing(payload, file.getName()));
+            }
         }
         return out;
     }
 
+    @Override
+    public int getCount() {
+        final File[] fileArray = storeDir.listFiles(uuidFilenameFilter);
+        if (fileArray == null) {
+            Log.w(LOGTAG, "listFiles unexpectedly returned null - unable to retrieve pings. Assuming 0. " +
+                    "Debug: exists? " + storeDir.exists() + "; directory? " + storeDir.isDirectory());
+            return 0;
+        }
+        return fileArray.length;
+    }
+
+    @Override
+    public Set<String> getStoredIDs() {
+        final Set<String> ids = new HashSet<>();
+        final File[] fileArray = storeDir.listFiles(uuidFilenameFilter);
+        if (fileArray == null) {
+            return ids;
+        }
+        // Map list of files to a set of IDs.
+        for (File file : fileArray) {
+            ids.add(file.getName());
+        }
+        return ids;
+    }
+
     /**
      * Logs if there is an error.
      *
      * @return the JSON object from the given file or null if there is an error.
      */
     private JSONObject lockAndReadJSONFromFile(final File file) {
         // lockAndReadFileAndCloseStream doesn't handle file size of 0.
         if (file.length() == 0) {
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java
@@ -2,16 +2,20 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, you can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 package org.mozilla.gecko.telemetry.stores;
 
 import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
 import org.mozilla.gecko.telemetry.TelemetryPing;
 
 import java.io.IOException;
 import java.util.List;
 import java.util.Set;
 
 /**
  * Persistent storage for TelemetryPings that are queued for upload.
@@ -21,33 +25,40 @@ import java.util.Set;
  * to synchronize state (or be stateless!).
  *
  * The pings in {@link #getAllPings()} and {@link #maybePrunePings()} are returned in the
  * same order in order to guarantee consistent results.
  */
 public abstract class TelemetryPingStore implements Parcelable {
     private final String profileName;
 
+    @VisibleForTesting
     public TelemetryPingStore(final String profileName) {
         this.profileName = profileName;
     }
 
     /**
      * @return the profile name associated with this store.
      */
     public String getProfileName() {
         return profileName;
     }
 
     /**
-     * @return a list of all the telemetry pings in the store that are ready for upload, ascending oldest to newest.
+     * @return a list of all the telemetry pings in the store, ascending oldest to newest. Depending
+     * on the store.
      */
     public abstract List<TelemetryPing> getAllPings();
 
     /**
+     * @return a number of all currently stored pings.
+     */
+    public abstract int getCount();
+
+    /**
      * Save a ping to the store.
      *
      * @param ping the ping to store
      * @throws IOException for underlying store access errors
      */
     public abstract void storePing(TelemetryPing ping) throws IOException;
 
     /**
@@ -58,9 +69,14 @@ public abstract class TelemetryPingStore
 
     /**
      * Removes the successfully uploaded pings from the database and performs another other actions necessary
      * for when upload is completed.
      *
      * @param successfulRemoveIDs doc ids of pings that were successfully uploaded
      */
     public abstract void onUploadAttemptComplete(Set<String> successfulRemoveIDs);
+
+    /**
+     * Returns a set of currently stored IDs.
+     */
+    public abstract Set<String> getStoredIDs();
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -893,24 +893,31 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'tabs/TabsTouchHelperCallback.java',
     'Telemetry.java',
     'telemetry/measurements/CampaignIdMeasurements.java',
     'telemetry/measurements/SearchCountMeasurements.java',
     'telemetry/measurements/SessionMeasurements.java',
     'telemetry/pingbuilders/TelemetryCorePingBuilder.java',
+    'telemetry/pingbuilders/TelemetryLocalPingBuilder.java',
     'telemetry/pingbuilders/TelemetryPingBuilder.java',
+    'telemetry/pingbuilders/TelemetrySyncEventPingBuilder.java',
+    'telemetry/pingbuilders/TelemetrySyncPingBuilder.java',
+    'telemetry/pingbuilders/TelemetrySyncPingBundleBuilder.java',
     'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java',
     'telemetry/schedulers/TelemetryUploadScheduler.java',
     'telemetry/stores/TelemetryJSONFilePingStore.java',
     'telemetry/stores/TelemetryPingStore.java',
+    'telemetry/TelemetryBackgroundReceiver.java',
     'telemetry/TelemetryConstants.java',
     'telemetry/TelemetryCorePingDelegate.java',
     'telemetry/TelemetryDispatcher.java',
+    'telemetry/TelemetryLocalPing.java',
+    'telemetry/TelemetryOutgoingPing.java',
     'telemetry/TelemetryPing.java',
     'telemetry/TelemetryPreferences.java',
     'telemetry/TelemetryUploadService.java',
     'TelemetryContract.java',
     'text/FloatingActionModeCallback.java',
     'text/FloatingToolbarTextSelection.java',
     'text/TextAction.java',
     'text/TextSelection.java',
deleted file mode 100644
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.background.common.telemetry;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-import org.mozilla.gecko.background.common.log.Logger;
-
-/**
- * Android Background Services are normally built into Fennec, but can also be
- * built as a stand-alone APK for rapid local development. The current Telemetry
- * implementation is coupled to Gecko, and Background Services should not
- * interact with Gecko directly. To maintain this independence, Background
- * Services lazily introspects the relevant Telemetry class from the enclosing
- * package, warning but otherwise ignoring failures during introspection or
- * invocation.
- * <p>
- * It is possible that Background Services will introspect and invoke the
- * Telemetry implementation while Gecko is not running. In this case, the Fennec
- * process itself buffers Telemetry events until such time as they can be
- * flushed to disk and uploaded. <b>There is no guarantee that all Telemetry
- * events will be uploaded!</b> Depending on the volume of data and the
- * application lifecycle, Telemetry events may be dropped.
- */
-public class TelemetryWrapper {
-  private static final String LOG_TAG = TelemetryWrapper.class.getSimpleName();
-
-  // Marking this volatile maintains thread safety cheaply.
-  private static volatile Method mAddToHistogram;
-
-  public static void addToHistogram(String key, int value) {
-    if (mAddToHistogram == null) {
-      try {
-        final Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
-        mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
-      } catch (ClassNotFoundException e) {
-        Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry class found!");
-        return;
-      } catch (NoSuchMethodException e) {
-        Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry.addToHistogram(String, int) method not found!");
-        return;
-      }
-    }
-
-    if (mAddToHistogram != null) {
-      try {
-        mAddToHistogram.invoke(null, key, value);
-      } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
-        Logger.warn(LOG_TAG, "Got exception invoking telemetry!");
-      }
-    }
-  }
-}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java
@@ -8,24 +8,22 @@ import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationCompat.Builder;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.Action;
-import org.mozilla.gecko.sync.telemetry.TelemetryContract;
 
 /**
  * Abstraction that manages notifications shown or hidden for a Firefox Account.
  * <p>
  * In future, we anticipate this tracking things like:
  * <ul>
  * <li>new engines to offer to Sync;</li>
  * <li>service interruption updates;</li>
@@ -79,17 +77,17 @@ public class FxAccountNotificationManage
       localeUpdated = true;
       Locales.getLocaleManager().getAndApplyPersistedLocale(context);
     }
 
     final String title;
     final String text;
     final Intent notificationIntent;
     if (action == Action.NeedsFinishMigrating) {
-      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1);
+      //TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1);
 
       title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
       text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
       notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
     } else {
       title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
       text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
       notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -1,28 +1,27 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.sync;
 
 import android.accounts.Account;
 import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SyncResult;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.text.TextUtils;
+import android.support.v4.content.LocalBroadcastManager;
 
 import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.devices.FxAccountDeviceListUpdater;
 import org.mozilla.gecko.fxa.devices.FxAccountDeviceRegistrator;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
@@ -42,16 +41,17 @@ import org.mozilla.gecko.sync.SyncConfig
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 import org.mozilla.gecko.sync.telemetry.TelemetryContract;
 import org.mozilla.gecko.tokenserver.TokenServerClient;
 import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
 import org.mozilla.gecko.tokenserver.TokenServerException;
 import org.mozilla.gecko.tokenserver.TokenServerToken;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -81,35 +81,35 @@ public class FxAccountSyncAdapter extend
   // Non-user initiated sync can't take longer than 30 minutes.
   // To ensure we're not churning through device's battery/resources, we limit sync to 10 minutes,
   // and request a re-sync if we hit that deadline.
   private static final long SYNC_DEADLINE_DELTA_MILLIS = TimeUnit.MINUTES.toMillis(10);
 
   protected final ExecutorService executor;
   protected final FxAccountNotificationManager notificationManager;
 
+  private final TelemetryCollector telemetryCollector = new TelemetryCollector();
+
   public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
     super(context, autoInitialize);
     this.executor = Executors.newSingleThreadExecutor();
     this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
   }
 
   protected static class SyncDelegate extends FxAccountSyncDelegate {
     @Override
     public void handleSuccess() {
       Logger.info(LOG_TAG, "Sync succeeded.");
       super.handleSuccess();
-      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1);
     }
 
     @Override
     public void handleError(Exception e) {
       Logger.error(LOG_TAG, "Got exception syncing.", e);
       super.handleError(e);
-      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1);
     }
 
     @Override
     public void handleCannotSync(State finalState) {
       Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
       super.handleCannotSync(finalState);
     }
 
@@ -141,16 +141,79 @@ public class FxAccountSyncAdapter extend
       this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
     }
 
     public Collection<String> getStageNamesToSync() {
       return this.stageNamesToSync;
     }
   }
 
+  /**
+   * Locally broadcasts telemetry gathered by GlobalSession, so that it may be processed by
+   * Java Telemetry - specifically, {@link org.mozilla.gecko.telemetry.TelemetryBackgroundReceiver}.
+   * Due to how packages are built, we can not call into it directly from *.sync.
+   */
+  private static class InstrumentedSessionCallback extends SessionCallback {
+    private static final String ACTION_BACKGROUND_TELEMETRY = "org.mozilla.gecko.telemetry.BACKGROUND";
+
+    private final LocalBroadcastManager localBroadcastManager;
+    private final TelemetryCollector telemetryCollector;
+
+    InstrumentedSessionCallback(TelemetryCollector telemetryCollector, LocalBroadcastManager localBroadcastManager, SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) {
+      super(syncDelegate, schedulePolicy);
+      this.telemetryCollector = telemetryCollector;
+      this.localBroadcastManager = localBroadcastManager;
+    }
+
+    @Override
+    public void handleSuccess(GlobalSession globalSession) {
+      super.handleSuccess(globalSession);
+      recordTelemetry();
+    }
+
+    @Override
+    public void handleError(GlobalSession globalSession, Exception ex, String reason) {
+      super.handleError(globalSession, ex, reason);
+      this.telemetryCollector.setError(TelemetryCollector.KEY_ERROR_INTERNAL, reason);
+      recordTelemetry();
+    }
+
+    @Override
+    public void handleError(GlobalSession globalSession, Exception e) {
+      super.handleError(globalSession, e);
+      if (e instanceof TokenServerException) {
+        this.telemetryCollector.setError(
+                TelemetryCollector.KEY_ERROR_TOKEN, e.getClass().getSimpleName());
+      } else {
+        this.telemetryCollector.setError(
+                TelemetryCollector.KEY_ERROR_INTERNAL, e.getClass().getSimpleName());
+      }
+      recordTelemetry();
+    }
+
+    @Override
+    public void handleAborted(GlobalSession globalSession, String reason) {
+      super.handleAborted(globalSession, reason);
+      // Note to future maintainers: while there are reasons, other than 'backoff', this method
+      // might be called, in practice that _is_ the only reason it gets called at the moment of
+      // writing this. If this changes, please do expand this telemetry handling.
+      this.telemetryCollector.setError(TelemetryCollector.KEY_ERROR_INTERNAL, "backoff");
+      recordTelemetry();
+    }
+
+    private void recordTelemetry() {
+      telemetryCollector.setFinished(SystemClock.elapsedRealtime());
+      final Intent telemetryIntent = new Intent();
+      telemetryIntent.setAction(ACTION_BACKGROUND_TELEMETRY);
+      telemetryIntent.putExtra(TelemetryContract.KEY_TYPE, TelemetryContract.KEY_TYPE_SYNC);
+      telemetryIntent.putExtra(TelemetryContract.KEY_TELEMETRY, this.telemetryCollector.build());
+      localBroadcastManager.sendBroadcast(telemetryIntent);
+    }
+  }
+
   protected static class SessionCallback implements GlobalSessionCallback {
     protected final SyncDelegate syncDelegate;
     protected final SchedulePolicy schedulePolicy;
     protected volatile BackoffHandler storageBackoffHandler;
 
     public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) {
       this.syncDelegate = syncDelegate;
       this.schedulePolicy = schedulePolicy;
@@ -222,16 +285,21 @@ public class FxAccountSyncAdapter extend
         this.schedulePolicy.onSuccessfulSync(otherClientsCount);
       } finally {
         // Continue with the usual success flow.
         syncDelegate.handleSuccess();
       }
     }
 
     @Override
+    public void handleError(GlobalSession globalSession, Exception ex, String reason) {
+      this.handleError(globalSession, ex);
+    }
+
+    @Override
     public void handleError(GlobalSession globalSession, Exception e) {
       Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below.
       syncDelegate.handleError(e);
       // TODO: should we reduce the periodic sync interval?
     }
 
     @Override
     public void handleAborted(GlobalSession globalSession, String reason) {
@@ -349,17 +417,19 @@ public class FxAccountSyncAdapter extend
 
           final Context context = getContext();
           final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
 
           Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
           syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
           syncConfig.setClusterURL(storageServerURI);
 
-          globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate);
+          globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate, telemetryCollector);
+          telemetryCollector.setIDs(token.hashedFxaUid, clientsDataDelegate.getAccountGUID());
+          telemetryCollector.setStarted(SystemClock.elapsedRealtime());
           globalSession.start(syncDeadline);
         } catch (Exception e) {
           callback.handleError(globalSession, e);
           return;
         }
       }
 
       @Override
@@ -461,17 +531,16 @@ public class FxAccountSyncAdapter extend
 
     FirefoxAccounts.logSyncOptions(extras);
 
     if (this.lastSyncRealtimeMillis > 0L &&
         (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime() &&
             !extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) {
       Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
                            ": minimum interval not met.");
-      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1);
       return;
     }
 
     // Pickle in a background thread to avoid strict mode warnings.
     ThreadPool.run(new Runnable() {
       @Override
       public void run() {
         try {
@@ -541,18 +610,16 @@ public class FxAccountSyncAdapter extend
       try {
         state = fxAccount.getState();
       } catch (Exception e) {
         fxAccount.releaseSharedAccountStateLock();
         syncDelegate.handleError(e);
         return;
       }
 
-      TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1);
-
       final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
       stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
         @Override
         public void handleNotMarried(State notMarried) {
           Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel());
           schedulePolicy.onHandleFinal(notMarried.getNeededAction());
           syncDelegate.handleCannotSync(notMarried);
           if (notMarried.getStateLabel() == StateLabel.Engaged) {
@@ -596,17 +663,23 @@ public class FxAccountSyncAdapter extend
             if (!shouldRequestToken(tokenBackoffHandler, extras)) {
               Logger.info(LOG_TAG, "Not syncing (token server).");
               syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
               return;
             }
 
             onSessionTokenStateReached(context, fxAccount);
 
-            final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
+            final SessionCallback sessionCallback = new InstrumentedSessionCallback(
+                    telemetryCollector,
+                    LocalBroadcastManager.getInstance(context),
+                    syncDelegate,
+                    schedulePolicy
+            );
+
             final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
             final String clientState = married.getClientState();
             syncWithAssertion(
                     assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs,
                     syncKeyBundle, clientState, sessionCallback, extras, fxAccount, syncDeadline);
 
             // Force fetch the profile avatar information. (asynchronous, in another thread)
             Logger.info(LOG_TAG, "Fetching profile avatar information.");
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync;
 
 import android.content.Context;
+import android.os.SystemClock;
 import android.support.annotation.VisibleForTesting;
 
 import org.json.simple.JSONArray;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
@@ -38,16 +39,18 @@ import org.mozilla.gecko.sync.stage.Fetc
 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
 import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
 
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumMap;
@@ -79,16 +82,22 @@ public class GlobalSession implements Ht
   private long syncDeadline;
 
   /**
    * Map from engine name to new settings for an updated meta/global record.
    * Engines to remove will have <code>null</code> EngineSettings.
    */
   public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>();
 
+  private final TelemetryCollector telemetryCollector;
+
+  public TelemetryCollector getTelemetryCollector() {
+    return telemetryCollector;
+  }
+
    /*
    * Key accessors.
    */
   public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
     return config.getCollectionKeys().keyBundleForCollection(collection);
   }
 
   /*
@@ -100,26 +109,28 @@ public class GlobalSession implements Ht
 
   public URI wboURI(String collection, String id) throws URISyntaxException {
     return config.wboURI(collection, id);
   }
 
   public GlobalSession(SyncConfiguration config,
                        GlobalSessionCallback callback,
                        Context context,
-                       ClientsDataDelegate clientsDelegate)
+                       ClientsDataDelegate clientsDelegate,
+                       TelemetryCollector telemetryCollector)
     throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
 
     if (callback == null) {
       throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
     }
 
     this.callback        = callback;
     this.context         = context;
     this.clientsDelegate = clientsDelegate;
+    this.telemetryCollector = telemetryCollector;
 
     this.config = config;
     registerCommands();
     prepareStages();
 
     if (config.stagesToSync == null) {
       Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names.");
       config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames());
@@ -276,22 +287,37 @@ public class GlobalSession implements Ht
     try {
       nextStage = this.getSyncStageByName(next);
     } catch (NoSuchStageException e) {
       this.abort(e, "No such stage " + next);
       return;
     }
     this.currentState = next;
     Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")...");
+
+    // For named stages, use the repository name.
+    String collectorName = currentState.getRepositoryName();
+    // For unnamed, non-repository stages use name of the stage itself.
+    if (collectorName == null) {
+      collectorName = currentState.name();
+    }
+    final TelemetryStageCollector stageCollector = telemetryCollector.collectorFor(collectorName);
+    // Stage is responsible for setting the 'finished' timestamp when appropriate.
+    stageCollector.started = SystemClock.elapsedRealtime();
+
     try {
-      nextStage.execute(this);
+      nextStage.execute(this, stageCollector);
     } catch (Exception ex) {
       Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next);
+      // We're not setting stageCollector's error since there's a chance the stage already set it
+      // and we'll lose a root cause error by overriding it here. Call to `abort` will end up calling
+      // GlobalSession's callback handler which is instrumented and records global errors, so this
+      // error won't get lost.
+      stageCollector.finished = SystemClock.elapsedRealtime();
       this.abort(ex, "Uncaught exception in stage.");
-      return;
     }
   }
 
   public Context getContext() {
     return this.context;
   }
 
   /**
@@ -318,16 +344,17 @@ public class GlobalSession implements Ht
     this.advance();
   }
 
   /**
    * Stop this sync and start again.
    * @throws AlreadySyncingException
    */
   protected void restart() throws AlreadySyncingException {
+    telemetryCollector.setRestarted();
     this.currentState = GlobalSyncStage.Stage.idle;
     if (callback.shouldBackOffStorage()) {
       this.callback.handleAborted(this, "Told to back off.");
       return;
     }
     // Restart with the same deadline as before.
     this.start(syncDeadline);
   }
@@ -494,17 +521,17 @@ public class GlobalSession implements Ht
       callback.requestBackoff(existingBackoff);
     }
     if (!(e instanceof HTTPFailureException)) {
       //  e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record.
       if (this.hasUpdatedMetaGlobal()) {
         this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort.
       }
     }
-    this.callback.handleError(this, e);
+    this.callback.handleError(this, e, reason);
   }
 
   public void handleIncompleteStage() {
     // Let our delegate know that current stage is incomplete and needs to be synced again.
     callback.handleIncompleteStage(this.currentState, this);
   }
 
   public void handleHTTPError(SyncStorageResponse response, String reason) {
@@ -631,22 +658,23 @@ public class GlobalSession implements Ht
     }
 
     request.put(keysRecord);
   }
 
   /*
    * meta/global callbacks.
    */
-  public void processMetaGlobal(MetaGlobal global) {
+  public void processMetaGlobal(MetaGlobal global, TelemetryStageCollector stageCollector) {
     config.metaGlobal = global;
 
     Long storageVersion = global.getStorageVersion();
     if (storageVersion == null) {
       Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version.");
+      stageCollector.error = new TelemetryCollector.StageErrorBuilder("metaglobal", "noversion").build();
       freshStart();
       return;
     }
     if (storageVersion < STORAGE_VERSION) {
       Logger.warn(LOG_TAG, "Outdated server: reported " +
           "remote storage version " + storageVersion + " < " +
           "local storage version " + STORAGE_VERSION);
       freshStart();
@@ -657,16 +685,17 @@ public class GlobalSession implements Ht
           "remote storage version " + storageVersion + " > " +
           "local storage version " + STORAGE_VERSION);
       requiresUpgrade();
       return;
     }
     String remoteSyncID = global.getSyncID();
     if (remoteSyncID == null) {
       Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID.");
+      stageCollector.error = new TelemetryCollector.StageErrorBuilder("metaglobal", "nosyncid").build();
       freshStart();
       return;
     }
     String localSyncID = config.syncID;
     if (!remoteSyncID.equals(localSyncID)) {
       Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID.");
       resetAllStages();
       config.purgeCryptoKeys();
@@ -1103,17 +1132,17 @@ public class GlobalSession implements Ht
 
   /**
    * Suggest that your Sync client needs to be upgraded to work
    * with this server.
    */
   public void requiresUpgrade() {
     Logger.info(LOG_TAG, "Client outdated storage version; requires update.");
     // TODO: notify UI.
-    this.abort(null, "Requires upgrade");
+    this.abort(null, "Requires upgrade from " + STORAGE_VERSION);
   }
 
   /**
    * If meta/global is missing or malformed, throws a MetaGlobalException.
    * Otherwise, returns true if there is an entry for this engine in the
    * meta/global "engines" object.
    * <p>
    * This is a global/permanent setting, not a local/temporary setting. For the
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java
@@ -30,16 +30,17 @@ public interface GlobalSessionCallback {
   /**
    * Called when a migration sentinel has been found and processed successfully.
    * <p>
    * This account should stop syncing immediately, and arrange to delete itself.
    */
   void informMigrated(GlobalSession session);
 
   void handleAborted(GlobalSession globalSession, String reason);
+  void handleError(GlobalSession globalSession, Exception ex, String reason);
   void handleError(GlobalSession globalSession, Exception ex);
   void handleSuccess(GlobalSession globalSession);
   void handleStageCompleted(Stage currentState, GlobalSession globalSession);
   void handleIncompleteStage(Stage currentState, GlobalSession globalSession);
   void handleFullSyncNecessary();
 
   /**
    * Called when a {@link GlobalSession} wants to know if it should continue
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java
@@ -458,16 +458,20 @@ public abstract class AndroidBrowserRepo
               trace("Remote modified, local not. Deleting.");
               storeRecordDeletion(record, existingRecord);
               return;
             }
 
             trace("Both local and remote records have been modified.");
             if (record.lastModified > existingRecord.lastModified) {
               trace("Remote is newer, and deleted. Deleting local.");
+              // Note that while this counts as "reconciliation", we're probably over-counting.
+              // Currently, locallyModified above is _always_ true if a record exists locally,
+              // and so we'll consider any deletions of already present records as reconciliations.
+              storeDelegate.onRecordStoreReconciled(record.guid);
               storeRecordDeletion(record, existingRecord);
               return;
             }
 
             trace("Remote is older, local is not deleted. Ignoring.");
             return;
           }
           // End deletion logic.
@@ -512,16 +516,17 @@ public abstract class AndroidBrowserRepo
                        (toStore.deleted ? " with deleted record " : " with record ") +
                        toStore.guid);
           Record replaced = replace(toStore, existingRecord);
 
           // Note that we don't track records here; deciding that is the job
           // of reconcileRecords.
           Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
                                 "(" + replaced.androidID + ")");
+          storeDelegate.onRecordStoreReconciled(replaced.guid);
           storeDelegate.onRecordStoreSucceeded(replaced.guid);
           return;
 
         } catch (MultipleRecordsForGuidException e) {
           Logger.error(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
           storeDelegate.onRecordStoreFailed(e, record.guid);
           return;
         } catch (NoGuidForIdException e) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
@@ -607,16 +607,20 @@ public class FormHistoryRepositorySessio
               return;
             }
 
             Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
             if (record.lastModified > existingRecord.lastModified) {
               Logger.trace(LOG_TAG, "Remote is newer, and deleted. Purging local.");
               deleteExistingRecord(existingRecord);
               trackRecord(record);
+              // Note that while this counts as "reconciliation", we're probably over-counting.
+              // Currently, locallyModified above is _always_ true if a record exists locally,
+              // and so we'll consider any deletions of already present records as reconciliations.
+              storeDelegate.onRecordStoreReconciled(record.guid);
               storeDelegate.onRecordStoreSucceeded(record.guid);
               return;
             }
 
             Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
             return;
           }
           // End deletion logic.
@@ -658,16 +662,17 @@ public class FormHistoryRepositorySessio
             return;
           }
 
           Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
           if (record.lastModified > existingRecord.lastModified) {
             Logger.trace(LOG_TAG, "Remote is newer, and not deleted. Storing.");
             replaceExistingRecordWithRegularRecord(record, existingRecord);
             trackRecord(record);
+            storeDelegate.onRecordStoreReconciled(record.guid);
             storeDelegate.onRecordStoreSucceeded(record.guid);
             return;
           }
 
           Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
         } catch (Exception e) {
           Logger.error(LOG_TAG, "Store failed for " + record.guid, e);
           storeDelegate.onRecordStoreFailed(e, record.guid);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
@@ -306,16 +306,20 @@ public class PasswordsRepositorySession 
             trace("Remote modified, local not. Deleting.");
             storeRecordDeletion(remoteRecord);
             return;
           }
 
           trace("Both local and remote records have been modified.");
           if (remoteRecord.lastModified > existingRecord.lastModified) {
             trace("Remote is newer, and deleted. Deleting local.");
+            // Note that while this counts as "reconciliation", we're probably over-counting.
+            // Currently, locallyModified above is _always_ true if a record exists locally,
+            // and so we'll consider any deletions of already present records as reconciliations.
+            storeDelegate.onRecordStoreReconciled(record.guid);
             storeRecordDeletion(remoteRecord);
             return;
           }
 
           trace("Remote is older, local is not deleted. Ignoring.");
 
           return;
         }
@@ -384,16 +388,17 @@ public class PasswordsRepositorySession 
           storeDelegate.onRecordStoreFailed(e, record.guid);
           return;
         }
 
         // Note that we don't track records here; deciding that is the job
         // of reconcileRecords.
         Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
                               "(" + replaced.androidID + ")");
+        storeDelegate.onRecordStoreReconciled(record.guid);
         storeDelegate.onRecordStoreSucceeded(record.guid);
         return;
       }
     };
     storeWorkQueue.execute(storeRunnable);
   }
 
   @Override
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
@@ -59,9 +59,19 @@ public class DeferredRepositorySessionSt
   public void onStoreFailed(final Exception e) {
     executor.execute(new Runnable() {
       @Override
       public void run() {
         inner.onStoreFailed(e);
       }
     });
   }
+
+  @Override
+  public void onRecordStoreReconciled(final String guid) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        inner.onRecordStoreReconciled(guid);
+      }
+    });
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
@@ -11,14 +11,19 @@ import java.util.concurrent.ExecutorServ
  * need help doing this.
  *
  * @author rnewman
  *
  */
 public interface RepositorySessionStoreDelegate {
   void onRecordStoreFailed(Exception ex, String recordGuid);
 
+  // Meant for signaling that a record has been reconciled.
+  // Only makes sense in context of local repositories.
+  // Further call to onRecordStoreSucceeded is necessary.
+  void onRecordStoreReconciled(String guid);
+
   // Called with a GUID when store has succeeded.
   void onRecordStoreSucceeded(String guid);
   void onStoreCompleted(long storeEnd);
   void onStoreFailed(Exception e);
   RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor);
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java
@@ -1,28 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
 import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
 
 /**
  * A global sync stage that manages a <code>GlobalSession</code> instance. This
  * class is intended to be temporary: it should disappear as work to make
  * data-driven syncs progresses.
  * <p>
  * This class is inherently <b>thread-unsafe</b>: if <code>session</code> is
  * mutated after being set, all sorts of bad things could occur. At the time of
  * writing, every <code>GlobalSyncStage</code> created is executed (wiped,
  * reset) with the same <code>GlobalSession</code> argument.
  */
 public abstract class AbstractSessionManagingSyncStage implements GlobalSyncStage {
   protected GlobalSession session;
+  protected TelemetryStageCollector telemetryStageCollector;
 
   protected abstract void execute() throws NoSuchStageException;
   protected abstract void resetLocal();
   protected abstract void wipeLocal() throws Exception;
 
   @Override
   public void resetLocal(GlobalSession session) {
     this.session = session;
@@ -31,13 +33,15 @@ public abstract class AbstractSessionMan
 
   @Override
   public void wipeLocal(GlobalSession session) throws Exception {
     this.session = session;
     wipeLocal();
   }
 
   @Override
-  public void execute(GlobalSession session) throws NoSuchStageException {
+  public void execute(GlobalSession session, TelemetryStageCollector telemetryStageCollector) throws NoSuchStageException {
+    this.telemetryStageCollector = telemetryStageCollector;
     this.session = session;
+
     execute();
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java
@@ -1,69 +1,90 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
+import android.os.SystemClock;
+
 import java.net.URISyntaxException;
 import java.util.HashSet;
 import java.util.Set;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.CollectionKeys;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 public class EnsureCrypto5KeysStage
 extends AbstractNonRepositorySyncStage
 implements SyncStorageRequestDelegate {
 
   private static final String LOG_TAG = "EnsureC5KeysStage";
   private static final String CRYPTO_COLLECTION = "crypto";
   protected boolean retrying = false;
 
+  private static final String TELEMETRY_ERROR_NAME = "keys";
+  private static final String TELEMETRY_ERROR_NO_COLLECTIONS = "nocollections";
+  private static final String TELEMETRY_ERROR_BAD_URI = "baduri";
+  private static final String TELEMETRY_ERROR_INVALID_WBO = "invalidwbo";
+  private static final String TELEMETRY_ERROR_REDOWNLOADING = "redownloading";
+  // While this isn't strictly an error, marking it as one is currently our best avenue for
+  // tracking these types of events given the current sync telemetry.
+  private static final String TELEMETRY_ERROR_KEYS_CHANGED = "changed";
+
   @Override
   public void execute() throws NoSuchStageException {
     InfoCollections infoCollections = session.config.infoCollections;
     if (infoCollections == null) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_NO_COLLECTIONS)
+              .build();
       session.abort(null, "No info/collections set in EnsureCrypto5KeysStage.");
       return;
     }
 
     PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
     long lastModified = pck.lastModified();
     if (retrying || !infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
       // Try to use our local collection keys for this session.
       Logger.debug(LOG_TAG, "Trying to use persisted collection keys for this session.");
       CollectionKeys keys = pck.keys();
       if (keys != null) {
         Logger.trace(LOG_TAG, "Using persisted collection keys for this session.");
         session.config.setCollectionKeys(keys);
-        session.advance();
+        doAdvance();
         return;
       }
       Logger.trace(LOG_TAG, "Failed to use persisted collection keys for this session.");
     }
 
     // We need an update: fetch fresh keys.
     Logger.debug(LOG_TAG, "Fetching fresh collection keys for this session.");
     try {
       SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys"));
       request.delegate = this;
       request.get();
     } catch (URISyntaxException e) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_BAD_URI)
+              .build();
       session.abort(e, "Invalid URI.");
     }
   }
 
   @Override
   public AuthHeaderProvider getAuthHeaderProvider() {
     return session.getAuthHeaderProvider();
   }
@@ -119,74 +140,101 @@ implements SyncStorageRequestDelegate {
       }
     }
 
     return changedKeys;
   }
 
   @Override
   public void handleRequestSuccess(SyncStorageResponse response) {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+
     // Take the timestamp from the response since it is later than the timestamp from info/collections.
     long responseTimestamp = response.normalizedWeaveTimestamp();
     CollectionKeys keys = new CollectionKeys();
     try {
       ExtendedJSONObject body = response.jsonObjectBody();
       if (Logger.LOG_PERSONAL_INFORMATION) {
         Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString());
       }
       keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle);
     } catch (Exception e) {
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_INVALID_WBO)
+              .build();
       session.abort(e, "Invalid keys WBO.");
       return;
     }
 
     PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
     if (!pck.persistedKeysExist()) {
       // New keys, and no old keys! Persist keys and server timestamp.
       Logger.trace(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified.");
       setAndPersist(pck, keys, responseTimestamp);
-      session.advance();
+      doAdvance();
       return;
     }
 
     // New keys, but we had old keys.  Check for differences.
     CollectionKeys oldKeys = pck.keys();
     Set<String> changedCollections = collectionsToUpdate(oldKeys, keys);
     if (!changedCollections.isEmpty()) {
       // New keys, different from old keys.
       Logger.trace(LOG_TAG, "Fetched keys are not the same as persisted keys; " +
           "setting fetched keys for this session before resetting changed engines.");
       setAndPersist(pck, keys, responseTimestamp);
       session.resetStagesByName(changedCollections);
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_KEYS_CHANGED)
+              .build();
       session.abort(null, "crypto/keys changed on server.");
       return;
     }
 
     // New keys don't differ from old keys; persist timestamp and move on.
     Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified.");
     session.config.setCollectionKeys(oldKeys);
     pck.persistLastModified(response.normalizedWeaveTimestamp());
-    session.advance();
+    doAdvance();
   }
 
   @Override
   public void handleRequestFailure(SyncStorageResponse response) {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+
     if (retrying) {
       // Should happen very rarely -- this means we uploaded our crypto/keys
       // successfully, but failed to re-download.
       session.handleHTTPError(response, "Failure while re-downloading already uploaded keys.");
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_REDOWNLOADING)
+              .setLastException(new HTTPFailureException(response))
+              .build();
       return;
     }
 
     int statusCode = response.getStatusCode();
     if (statusCode == 404) {
       Logger.info(LOG_TAG, "Got 404 fetching keys.  Fresh starting since keys are missing on server.");
       session.freshStart();
       return;
     }
+
+    telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+            .setLastException(new HTTPFailureException(response))
+            .build();
     session.handleHTTPError(response, "Failure fetching keys: got response status code " + statusCode);
   }
 
   @Override
   public void handleRequestError(Exception ex) {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+    telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+            .setLastException(ex)
+            .build();
     session.abort(ex, "Failure fetching keys.");
   }
+
+  private void doAdvance() {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+    session.advance();
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java
@@ -1,44 +1,61 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
+import android.os.SystemClock;
+
 import java.net.URISyntaxException;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 public class FetchInfoCollectionsStage extends AbstractNonRepositorySyncStage {
   public class StageInfoCollectionsDelegate implements JSONRecordFetchDelegate {
 
     @Override
     public void handleSuccess(ExtendedJSONObject global) {
       session.config.infoCollections = new InfoCollections(global);
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
       session.advance();
     }
 
     @Override
     public void handleFailure(SyncStorageResponse response) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+              .setLastException(new HTTPFailureException(response))
+              .build();
       session.handleHTTPError(response, "Failure fetching info/collections.");
     }
 
     @Override
     public void handleError(Exception e) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+              .setLastException(e)
+              .build();
       session.abort(e, "Failure fetching info/collections.");
     }
 
   }
 
   @Override
   public void execute() throws NoSuchStageException {
     try {
       session.fetchInfoCollections(new StageInfoCollectionsDelegate());
     } catch (URISyntaxException e) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+              .setLastException(e)
+              .build();
       session.abort(e, "Invalid URI.");
     }
   }
 
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
@@ -1,57 +1,80 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
+import android.os.SystemClock;
+
 import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.JSONRecordFetcher;
 import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 /**
  * Fetches configuration data from info/configurations endpoint.
  */
 public class FetchInfoConfigurationStage extends AbstractNonRepositorySyncStage {
     private final String configurationURL;
     private final AuthHeaderProvider authHeaderProvider;
 
+    private static final String TELEMETRY_ERROR_NAME = "infoconfig";
+    private static final String TELEMETRY_ERROR_MISSING = "missing";
+
     public FetchInfoConfigurationStage(final String configurationURL, final AuthHeaderProvider authHeaderProvider) {
         super();
         this.configurationURL = configurationURL;
         this.authHeaderProvider = authHeaderProvider;
     }
 
     public class StageInfoConfigurationDelegate implements JSONRecordFetchDelegate {
         @Override
         public void handleSuccess(final ExtendedJSONObject result) {
             session.config.infoConfiguration = new InfoConfiguration(result);
+            telemetryStageCollector.finished = SystemClock.elapsedRealtime();
             session.advance();
         }
 
         @Override
         public void handleFailure(final SyncStorageResponse response) {
+            telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+
             // Handle all non-404 failures upstream.
             if (response.getStatusCode() != 404) {
+                telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+                        .setLastException(new HTTPFailureException(response))
+                        .build();
                 session.handleHTTPError(response, "Failure fetching info/configuration");
                 return;
             }
 
             // End-point might not be available (404) if server is running an older version.
             // We will use default config values in this case.
+            // While this is not strictly an error in a sense that it's recoverable, going forward the
+            // expectation of having info/config endpoint will solidify, and it should be easy enough
+            // to interpret this error by correlating it to the current state of deployed servers.
+            telemetryStageCollector.error = new TelemetryCollector
+                    .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_MISSING)
+                    .build();
             session.config.infoConfiguration = new InfoConfiguration();
             session.advance();
         }
 
         @Override
         public void handleError(final Exception e) {
+            telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+            telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+                    .setLastException(e)
+                    .build();
             session.abort(e, "Failure fetching info/configuration");
         }
     }
     @Override
     public void execute() {
         final StageInfoConfigurationDelegate delegate = new StageInfoConfigurationDelegate();
         final JSONRecordFetcher fetcher = new JSONRecordFetcher(configurationURL, authHeaderProvider);
         fetcher.fetch(delegate);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java
@@ -1,76 +1,102 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
+import android.os.SystemClock;
+
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.MetaGlobal;
 import org.mozilla.gecko.sync.PersistedMetaGlobal;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 public class FetchMetaGlobalStage extends AbstractNonRepositorySyncStage {
   private static final String LOG_TAG = "FetchMetaGlobalStage";
   private static final String META_COLLECTION = "meta";
 
+  private static final String TELEMETRY_ERROR_NAME = "metaglobal";
+  private static final String TELEMETRY_ERROR_MISSING = "missing";
+  private static final String TELEMETRY_ERROR_NO_INFO_COLLECTIONS = "noic";
+
   public class StageMetaGlobalDelegate implements MetaGlobalDelegate {
 
     private final GlobalSession session;
     public StageMetaGlobalDelegate(GlobalSession session) {
       this.session = session;
     }
 
     @Override
     public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
       Logger.trace(LOG_TAG, "Persisting fetched meta/global and last modified.");
       PersistedMetaGlobal pmg = session.config.persistedMetaGlobal();
       pmg.persistMetaGlobal(global);
       // Take the timestamp from the response since it is later than the timestamp from info/collections.
       pmg.persistLastModified(response.normalizedWeaveTimestamp());
 
-      session.processMetaGlobal(global);
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      session.processMetaGlobal(global, telemetryStageCollector);
     }
 
     @Override
     public void handleFailure(SyncStorageResponse response) {
+      telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+              .setLastException(new HTTPFailureException(response))
+              .build();
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
       session.handleHTTPError(response, "Failure fetching meta/global.");
     }
 
     @Override
     public void handleError(Exception e) {
+      telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+              .setLastException(e)
+              .build();
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
       session.abort(e, "Failure fetching meta/global.");
     }
 
     @Override
     public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+      // While not strictly an error, it's good to keep track of this.
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_MISSING)
+              .build();
       session.processMissingMetaGlobal(global);
     }
   }
 
   @Override
   public void execute() throws NoSuchStageException {
     InfoCollections infoCollections = session.config.infoCollections;
     if (infoCollections == null) {
+      telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+      telemetryStageCollector.error = new TelemetryCollector
+              .StageErrorBuilder(TELEMETRY_ERROR_NAME, TELEMETRY_ERROR_NO_INFO_COLLECTIONS)
+              .build();
       session.abort(null, "No info/collections set in FetchMetaGlobalStage.");
       return;
     }
 
     final long lastModified = session.config.persistedMetaGlobal().lastModified();
     if (!infoCollections.updateNeeded(META_COLLECTION, lastModified)) {
       // Try to use our local collection keys for this session.
       Logger.info(LOG_TAG, "Trying to use persisted meta/global for this session.");
       MetaGlobal global = session.config.persistedMetaGlobal().metaGlobal(session.config.metaURL(), session.getAuthHeaderProvider());
       if (global != null) {
         Logger.info(LOG_TAG, "Using persisted meta/global for this session.");
-        session.processMetaGlobal(global); // Calls session.advance().
+        telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+        session.processMetaGlobal(global, telemetryStageCollector); // Calls session.advance().
         return;
       }
       Logger.info(LOG_TAG, "Failed to use persisted meta/global for this session.");
     }
 
     // We need an update: fetch or upload meta/global as necessary.
     // We assert when we believe meta/global was last modified via X-I-U-S.
     Logger.info(LOG_TAG, "Fetching fresh meta/global for this session.");
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
@@ -1,21 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
+import android.support.annotation.Nullable;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
 
 
 public interface GlobalSyncStage {
   public static enum Stage {
     idle,                       // Start state.
     checkPreconditions,         // Preparation of the basics. TODO: clear status
     fetchInfoCollections,       // Take a look at timestamps.
     fetchInfoConfiguration,     // Fetch server upload limits
@@ -62,30 +65,30 @@ public interface GlobalSyncStage {
      * @return an immutable collection of Stages.
      */
     public static Collection<Stage> getNamedStages() {
       return Collections.unmodifiableCollection(named.values());
     }
 
     // Each Stage tracks its repositoryName.
     private final String repositoryName;
-    public String getRepositoryName() {
+    @Nullable public String getRepositoryName() {
       return repositoryName;
     }
 
     private Stage() {
       this.repositoryName = null;
     }
 
     private Stage(final String name) {
       this.repositoryName = name;
     }
   }
 
-  public void execute(GlobalSession session) throws NoSuchStageException;
+  void execute(GlobalSession session, TelemetryStageCollector telemetryStageCollector) throws NoSuchStageException;
   public void resetLocal(GlobalSession session);
   public void wipeLocal(GlobalSession session) throws Exception;
 
   /**
    * What storage version number this engine supports.
    * <p>
    * Used to generate a fresh meta/global record for upload.
    * @return a version number or <code>null</code> to never include this engine in a fresh meta/global record.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
@@ -37,16 +37,17 @@ import org.mozilla.gecko.sync.repositori
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 
 /**
  * Fetch from a server collection into a local repository, encrypting
@@ -644,20 +645,39 @@ public abstract class ServerSyncStage ex
     if (newConfig != null) {
       persistConfig(newConfig);
     } else {
       Logger.warn(LOG_TAG, "Didn't get configuration from synchronizer after success.");
     }
 
     final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
     int inboundCount = synchronizerSession.getInboundCount();
+    int inboundCountStored = synchronizerSession.getInboundCountStored();
+    int inboundCountFailed = synchronizerSession.getInboundCountFailed();
+    int inboundCountReconciled = synchronizerSession.getInboundCountReconciled();
     int outboundCount = synchronizerSession.getOutboundCount();
-    Logger.info(LOG_TAG, "Stage " + getEngineName() +
-        " received " + inboundCount + " and sent " + outboundCount +
-        " records in " + getStageDurationString() + ".");
+    int outboundCountStored = synchronizerSession.getOutboundCountStored();
+    int outboundCountFailed = synchronizerSession.getOutboundCountFailed();
+
+    telemetryStageCollector.finished = stageCompleteTimestamp;
+    telemetryStageCollector.inbound = inboundCount;
+    telemetryStageCollector.inboundStored = inboundCountStored;
+    telemetryStageCollector.inboundFailed = inboundCountFailed;
+    telemetryStageCollector.reconciled = inboundCountReconciled;
+    telemetryStageCollector.outbound = outboundCount;
+    telemetryStageCollector.outboundStored = outboundCountStored;
+    telemetryStageCollector.outboundFailed = outboundCountFailed;
+
+    Logger.info(LOG_TAG, "Stage " + getEngineName()
+            + " received " + inboundCount
+            + "; stored " + inboundCountStored + ", reconciling " + inboundCountReconciled
+            + " and failed to store " + inboundCountFailed
+            + ". Sent " + outboundCount
+            + "; server accepted " + outboundCountStored + " and rejected " + outboundCountFailed
+            + ". Duration: " + getStageDurationString() + ".");
     Logger.info(LOG_TAG, "Advancing session.");
     session.advance();
   }
 
   /**
    * We failed to sync this engine! Do not persist timestamps (which means that
    * the next sync will include this sync's data), but do advance the session
    * (if we didn't get a Retry-After header).
@@ -665,16 +685,26 @@ public abstract class ServerSyncStage ex
    * @param synchronizer the <code>Synchronizer</code> that failed.
    */
   @Override
   public void onSynchronizeFailed(Synchronizer synchronizer,
                                   Exception lastException, String reason) {
     stageCompleteTimestamp = SystemClock.elapsedRealtime();
     Logger.warn(LOG_TAG, "Synchronize failed: " + reason, lastException);
 
+    final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+
+    telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+            .setLastException(lastException)
+            .setFetchException(synchronizerSession.getFetchFailedCauseException())
+            .setStoreException(synchronizerSession.getStoreFailedCauseException())
+            .build();
+
+    telemetryStageCollector.finished = stageCompleteTimestamp;
+
     // This failure could be due to a 503 or a 401 and it could have headers.
     // Interrogate the headers but only abort the global session if Retry-After header is set.
     if (lastException instanceof HTTPFailureException) {
       SyncStorageResponse response = ((HTTPFailureException)lastException).response;
       if (response.retryAfterInSeconds() > 0) {
         session.handleHTTPError(response, reason); // Calls session.abort().
         return;
       } else {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
 import android.accounts.Account;
 import android.content.Context;
+import android.os.SystemClock;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
@@ -48,16 +49,17 @@ import org.mozilla.gecko.sync.net.SyncSt
 import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate;
 import org.mozilla.gecko.sync.net.WBORequestDelegate;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.android.RepoUtils;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecordFactory;
 import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import ch.boye.httpclientandroidlib.HttpStatus;
 
 public class SyncClientsEngineStage extends AbstractSessionManagingSyncStage {
   private static final String LOG_TAG = "SyncClientsEngineStage";
 
   public static final String COLLECTION_NAME       = "clients";
   public static final String STAGE_NAME            = COLLECTION_NAME;
@@ -239,29 +241,29 @@ public class SyncClientsEngineStage exte
 
     @Override
     public void handleRequestFailure(SyncStorageResponse response) {
       BaseResource.consumeEntity(response); // We don't need the response at all, and any exception handling shouldn't need the response body.
       localAccountGUIDDownloaded = false;
 
       try {
         Logger.info(LOG_TAG, "Client upload failed. Aborting sync.");
-        session.abort(new HTTPFailureException(response), "Client download failed.");
+        doAbort(new HTTPFailureException(response), "Client download failed.");
       } finally {
         // Close the database upon failure.
         closeDataAccessor();
       }
     }
 
     @Override
     public void handleRequestError(Exception ex) {
       localAccountGUIDDownloaded = false;
       try {
         Logger.info(LOG_TAG, "Client upload error. Aborting sync.");
-        session.abort(ex, "Failure fetching client record.");
+        doAbort(ex, "Failure fetching client record.");
       } finally {
         // Close the database upon error.
         closeDataAccessor();
       }
     }
 
     @Override
     public void handleWBO(CryptoRecord record) {
@@ -271,20 +273,24 @@ public class SyncClientsEngineStage exte
         if (clientsDelegate.isLocalGUID(r.guid)) {
           Logger.info(LOG_TAG, "Local client GUID exists on server and was downloaded.");
           localAccountGUIDDownloaded = true;
           handleDownloadedLocalRecord(r);
         } else {
           // Only need to store record if it isn't our local one.
           wipeAndStore(r);
           addCommands(r);
+          // Note that we are downloading all client records during every sync. As such, telemetry
+          // will include every client currently present in the constellation of devices.
+          // See the downloadClientRecords method elsewhere in the file.
+          telemetryStageCollector.getSyncCollector().addDevice(r);
         }
         RepoUtils.logClient(r);
       } catch (Exception e) {
-        session.abort(e, "Exception handling client WBO.");
+        doAbort(e, "Exception handling client WBO.");
         return;
       }
     }
 
     @Override
     public KeyBundle keyBundle() {
       try {
         return session.keyBundleForCollection(COLLECTION_NAME);
@@ -329,17 +335,17 @@ public class SyncClientsEngineStage exte
 
       // X-Weave-Timestamp is the modified time of uploaded records.
       // Always persist this.
       final long responseTimestamp = response.normalizedWeaveTimestamp();
       Logger.trace(LOG_TAG, "Timestamp from header is: " + responseTimestamp);
 
       if (responseTimestamp == -1) {
         final String message = "Response did not contain a valid timestamp.";
-        session.abort(new RuntimeException(message), message);
+        doAbort(new RuntimeException(message), message);
         return;
       }
 
       BaseResource.consumeEntity(response);
       session.config.persistServerClientsTimestamp(responseTimestamp);
 
       // If we're not uploading our record, we're done here; just
       // clean up and finish.
@@ -349,17 +355,17 @@ public class SyncClientsEngineStage exte
         checkAndUpload();
         return;
       }
 
       // If we're processing our record, we have a little more cleanup
       // to do.
       shouldUploadLocalRecord = false;
       session.config.persistServerClientRecordTimestamp(responseTimestamp);
-      session.advance();
+      doAdvance();
     }
 
     @Override
     public void handleRequestFailure(SyncStorageResponse response) {
       int statusCode = response.getStatusCode();
 
       // If upload failed because of `ifUnmodifiedSince` then there are new
       // commands uploaded to our record. We must download and process them first.
@@ -367,31 +373,31 @@ public class SyncClientsEngineStage exte
           statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
           uploadAttemptsCount.incrementAndGet() > MAX_UPLOAD_FAILURE_COUNT) {
 
         Logger.debug(LOG_TAG, "Client upload failed. Aborting sync.");
         if (!currentlyUploadingLocalRecord) {
           modifiedClientsToUpload.clear(); // These will be redownloaded.
         }
         BaseResource.consumeEntity(response); // The exception thrown should need the response body.
-        session.abort(new HTTPFailureException(response), "Client upload failed.");
+        doAbort(new HTTPFailureException(response), "Client upload failed.");
         return;
       }
       Logger.trace(LOG_TAG, "Retrying upload…");
       // Preconditions:
       // shouldUploadLocalRecord == true &&
       // statusCode != 412 &&
       // uploadAttemptCount < MAX_UPLOAD_FAILURE_COUNT
       checkAndUpload();
     }
 
     @Override
     public void handleRequestError(Exception ex) {
       Logger.info(LOG_TAG, "Client upload error. Aborting sync.");
-      session.abort(ex, "Client upload failed.");
+      doAbort(ex, "Client upload failed.");
     }
 
     @Override
     public KeyBundle keyBundle() {
       try {
         return session.keyBundleForCollection(COLLECTION_NAME);
       } catch (NoCollectionKeysSetException e) {
         return null;
@@ -402,17 +408,17 @@ public class SyncClientsEngineStage exte
   @Override
   public void execute() throws NoSuchStageException {
     // We can be disabled just for this sync.
     boolean enabledThisSync = session.isEngineLocallyEnabled(STAGE_NAME);
     if (!enabledThisSync) {
       // These log messages look best when they match the messages in ServerSyncStage.
       Logger.debug(LOG_TAG, "Stage " + STAGE_NAME + " disabled just for this sync.");
       Logger.info(LOG_TAG, "Skipping stage " + STAGE_NAME + ".");
-      session.advance();
+      doAdvance();
       return;
     }
 
     if (shouldDownload()) {
       downloadClientRecords();   // Will kick off upload, too…
     } else {
       // Upload if necessary.
     }
@@ -586,17 +592,17 @@ public class SyncClientsEngineStage exte
     Logger.debug(LOG_TAG, "Uploading records: " + cryptoRecords.size());
     clientUploadDelegate.setUploadDetails(false);
     this.uploadClientRecords(cryptoRecords);
   }
 
   protected void checkAndUpload() {
     if (!shouldUpload()) {
       Logger.debug(LOG_TAG, "Not uploading client record.");
-      session.advance();
+      doAdvance();
       return;
     }
 
     final ClientRecord localClient = newLocalClientRecord(session.getClientsDelegate());
     clientUploadDelegate.setUploadDetails(true);
     CryptoRecord cryptoRecord = encryptClientRecord(localClient);
     if (cryptoRecord != null) {
       this.uploadClientRecord(cryptoRecord);
@@ -606,24 +612,24 @@ public class SyncClientsEngineStage exte
   protected CryptoRecord encryptClientRecord(ClientRecord recordToUpload) {
     // Generate CryptoRecord from ClientRecord to upload.
     final String encryptionFailure = "Couldn't encrypt new client record.";
 
     try {
       CryptoRecord cryptoRecord = recordToUpload.getEnvelope();
       cryptoRecord.keyBundle = clientUploadDelegate.keyBundle();
       if (cryptoRecord.keyBundle == null) {
-        session.abort(new NoCollectionKeysSetException(), "No collection keys set.");
+        doAbort(new NoCollectionKeysSetException(), "No collection keys set.");
         return null;
       }
       return cryptoRecord.encrypt();
     } catch (UnsupportedEncodingException e) {
-      session.abort(e, encryptionFailure + " Unsupported encoding.");
+      doAbort(e, encryptionFailure + " Unsupported encoding.");
     } catch (CryptoException e) {
-      session.abort(e, encryptionFailure);
+      doAbort(e, encryptionFailure);
     }
     return null;
   }
 
   public void clearRecordsToUpload() {
     try {
       getClientsDatabaseAccessor().wipeCommandsTable();
       modifiedClientsToUpload.clear();
@@ -639,46 +645,46 @@ public class SyncClientsEngineStage exte
     try {
       final URI getURI = session.config.collectionURI(COLLECTION_NAME, true);
       final SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(getURI);
       request.delegate = clientDownloadDelegate;
 
       Logger.trace(LOG_TAG, "Downloading client records.");
       request.get();
     } catch (URISyntaxException e) {
-      session.abort(e, "Invalid URI.");
+      doAbort(e, "Invalid URI.");
     }
   }
 
   protected void uploadClientRecords(JSONArray records) {
     Logger.trace(LOG_TAG, "Uploading " + records.size() + " client records.");
     try {
       final URI postURI = session.config.collectionURI(COLLECTION_NAME, false);
       final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI);
       request.delegate = clientUploadDelegate;
       request.post(records);
     } catch (URISyntaxException e) {
-      session.abort(e, "Invalid URI.");
+      doAbort(e, "Invalid URI.");
     } catch (Exception e) {
-      session.abort(e, "Unable to parse body.");
+      doAbort(e, "Unable to parse body.");
     }
   }
 
   /**
    * Upload a client record via HTTP POST to the parent collection.
    */
   protected void uploadClientRecord(CryptoRecord record) {
     Logger.debug(LOG_TAG, "Uploading client record " + record.guid);
     try {
       final URI postURI = session.config.collectionURI(COLLECTION_NAME);
       final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI);
       request.delegate = clientUploadDelegate;
       request.post(record);
     } catch (URISyntaxException e) {
-      session.abort(e, "Invalid URI.");
+      doAbort(e, "Invalid URI.");
     }
   }
 
   protected ClientDownloadDelegate makeClientDownloadDelegate() {
     return new ClientDownloadDelegate();
   }
 
   protected void wipeAndStore(ClientRecord record) {
@@ -686,9 +692,22 @@ public class SyncClientsEngineStage exte
     if (shouldWipe) {
       db.wipeClientsTable();
       shouldWipe = false;
     }
     if (record != null) {
       db.store(record);
     }
   }
+
+  private void doAdvance() {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+    session.advance();
+  }
+
+  private void doAbort(Exception e, String reason) {
+    telemetryStageCollector.finished = SystemClock.elapsedRealtime();
+    telemetryStageCollector.error = new TelemetryCollector.StageErrorBuilder()
+            .setLastException(e)
+            .build();
+    session.abort(e, reason);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
@@ -68,26 +68,33 @@ import org.mozilla.gecko.sync.repositori
 public class RecordsChannel implements
   RepositorySessionFetchRecordsDelegate,
   RepositorySessionStoreDelegate,
   RecordsConsumerDelegate,
   RepositorySessionBeginDelegate {
 
   private static final String LOG_TAG = "RecordsChannel";
   public RepositorySession source;
-  public RepositorySession sink;
+  private RepositorySession sink;
   private final RecordsChannelDelegate delegate;
   private long fetchEnd = -1;
 
   private volatile ReflowIsNecessaryException reflowException;
 
-  protected final AtomicInteger numFetched = new AtomicInteger();
-  protected final AtomicInteger numFetchFailed = new AtomicInteger();
-  protected final AtomicInteger numStored = new AtomicInteger();
-  protected final AtomicInteger numStoreFailed = new AtomicInteger();
+  private final AtomicInteger fetchedCount = new AtomicInteger();
+  private final AtomicInteger fetchFailedCount = new AtomicInteger();
+
+  // Expected value relationships:
+  // attempted = accepted + failed
+  // reconciled <= accepted <= attempted
+  // reconciled = accepted - `new`, where `new` is inferred.
+  private final AtomicInteger storeAttemptedCount = new AtomicInteger();
+  private final AtomicInteger storeAcceptedCount = new AtomicInteger();
+  private final AtomicInteger storeFailedCount = new AtomicInteger();
+  private final AtomicInteger storeReconciledCount = new AtomicInteger();
 
   public RecordsChannel(RepositorySession source, RepositorySession sink, RecordsChannelDelegate delegate) {
     this.source    = source;
     this.sink      = sink;
     this.delegate  = delegate;
   }
 
   /*
@@ -113,44 +120,52 @@ public class RecordsChannel implements
   }
 
   /**
    * Get the number of records fetched so far.
    *
    * @return number of fetches.
    */
   public int getFetchCount() {
-    return numFetched.get();
+    return fetchedCount.get();
   }
 
   /**
    * Get the number of fetch failures recorded so far.
    *
    * @return number of fetch failures.
    */
   public int getFetchFailureCount() {
-    return numFetchFailed.get();
+    return fetchFailedCount.get();
   }
 
   /**
    * Get the number of store attempts (successful or not) so far.
    *
    * @return number of stores attempted.
    */
-  public int getStoreCount() {
-    return numStored.get();
+  public int getStoreAttemptedCount() {
+    return storeAttemptedCount.get();
+  }
+
+  public int getStoreAcceptedCount() {
+    return storeAcceptedCount.get();
   }
 
   /**
    * Get the number of store failures recorded so far.
    *
    * @return number of store failures.
    */
   public int getStoreFailureCount() {
-    return numStoreFailed.get();
+    return storeFailedCount.get();
+  }
+
+  public int getStoreReconciledCount() {
+    return storeReconciledCount.get();
   }
 
   /**
    * Start records flowing through the channel.
    */
   public void flow() {
     if (!isReady()) {
       RepositorySession failed = source;
@@ -164,20 +179,22 @@ public class RecordsChannel implements
     if (!source.dataAvailable()) {
       Logger.info(LOG_TAG, "No data available: short-circuiting flow from source " + source);
       long now = System.currentTimeMillis();
       this.delegate.onFlowCompleted(this, now, now);
       return;
     }
 
     sink.setStoreDelegate(this);
-    numFetched.set(0);
-    numFetchFailed.set(0);
-    numStored.set(0);
-    numStoreFailed.set(0);
+    fetchedCount.set(0);
+    fetchFailedCount.set(0);
+    storeAttemptedCount.set(0);
+    storeAcceptedCount.set(0);
+    storeFailedCount.set(0);
+    storeReconciledCount.set(0);
     // Start a consumer thread.
     this.consumer = new ConcurrentRecordConsumer(this);
     ThreadPool.run(this.consumer);
     waitingForQueueDone = true;
     source.fetchSince(source.getLastSyncTimestamp(), this);
   }
 
   /**
@@ -186,40 +203,40 @@ public class RecordsChannel implements
    */
   public void beginAndFlow() throws InvalidSessionTransitionException {
     Logger.trace(LOG_TAG, "Beginning source.");
     source.begin(this);
   }
 
   @Override
   public void store(Record record) {
-    numStored.incrementAndGet();
+    storeAttemptedCount.incrementAndGet();
     try {
       sink.store(record);
     } catch (NoStoreDelegateException e) {
       Logger.error(LOG_TAG, "Got NoStoreDelegateException in RecordsChannel.store(). This should not occur. Aborting.", e);
       delegate.onFlowStoreFailed(this, e, record.guid);
     }
   }
 
   @Override
   public void onFetchFailed(Exception ex) {
     Logger.warn(LOG_TAG, "onFetchFailed. Calling for immediate stop.", ex);
-    numFetchFailed.incrementAndGet();
+    fetchFailedCount.incrementAndGet();
     if (ex instanceof ReflowIsNecessaryException) {
       setReflowException((ReflowIsNecessaryException) ex);
     }
     delegate.onFlowFetchFailed(this, ex);
     // Sink will be informed once consumer finishes.
     this.consumer.halt();
   }
 
   @Override
   public void onFetchedRecord(Record record) {
-    numFetched.incrementAndGet();
+    fetchedCount.incrementAndGet();
     this.toProcess.add(record);
     this.consumer.doNotify();
   }
 
   @Override
   public void onFetchCompleted(final long fetchEnd) {
     Logger.trace(LOG_TAG, "onFetchCompleted. Stopping consumer once stores are done.");
     Logger.trace(LOG_TAG, "Fetch timestamp is " + fetchEnd);
@@ -230,29 +247,36 @@ public class RecordsChannel implements
   @Override
   public void onBatchCompleted() {
     this.sink.storeFlush();
   }
 
   @Override
   public void onRecordStoreFailed(Exception ex, String recordGuid) {
     Logger.trace(LOG_TAG, "Failed to store record with guid " + recordGuid);
-    numStoreFailed.incrementAndGet();
+    storeFailedCount.incrementAndGet();
     this.consumer.stored();
     delegate.onFlowStoreFailed(this, ex, recordGuid);
     // TODO: abort?
   }
 
   @Override
   public void onRecordStoreSucceeded(String guid) {
     Logger.trace(LOG_TAG, "Stored record with guid " + guid);
+    storeAcceptedCount.incrementAndGet();
     this.consumer.stored();
   }
 
   @Override
+  public void onRecordStoreReconciled(String guid) {
+    Logger.trace(LOG_TAG, "Reconciled record with guid " + guid);
+    storeReconciledCount.incrementAndGet();
+  }
+
+  @Override
   public void consumerIsDoneFull() {
     Logger.trace(LOG_TAG, "Consumer is done, processed all records. Are we waiting for it? " + waitingForQueueDone);
     if (waitingForQueueDone) {
       waitingForQueueDone = false;
 
       // Now we'll be waiting for sink to call its delegate's onStoreCompleted or onStoreFailed.
       this.sink.storeDone();
     }
@@ -351,17 +375,16 @@ public class RecordsChannel implements
 
   @Override
   public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
     // Lie outright. We know that all of our fetch methods are safe.
     return this;
   }
 
   @Nullable
-  @VisibleForTesting
   public synchronized ReflowIsNecessaryException getReflowException() {
     return reflowException;
   }
 
   private synchronized void setReflowException(@NonNull ReflowIsNecessaryException e) {
     // It is a mistake to set reflow exception multiple times.
     if (reflowException != null) {
       throw new IllegalStateException("Reflow exception already set: " + reflowException);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
@@ -70,18 +70,26 @@ implements RecordsChannelDelegate,
   // that a concurrently syncing client has uploaded.
   private long pendingATimestamp = -1;
   private long pendingBTimestamp = -1;
   private long storeEndATimestamp = -1;
   private long storeEndBTimestamp = -1;
   private boolean flowAToBCompleted = false;
   private boolean flowBToACompleted = false;
 
-  protected final AtomicInteger numInboundRecords = new AtomicInteger(-1);
-  protected final AtomicInteger numOutboundRecords = new AtomicInteger(-1);
+  private final AtomicInteger numInboundRecords = new AtomicInteger(-1);
+  private final AtomicInteger numInboundRecordsStored = new AtomicInteger(-1);
+  private final AtomicInteger numInboundRecordsFailed = new AtomicInteger(-1);
+  private final AtomicInteger numInboundRecordsReconciled = new AtomicInteger(-1);
+  private final AtomicInteger numOutboundRecords = new AtomicInteger(-1);
+  private final AtomicInteger numOutboundRecordsStored = new AtomicInteger(-1);
+  private final AtomicInteger numOutboundRecordsFailed = new AtomicInteger(-1);
+
+  private Exception fetchFailedCauseException;
+  private Exception storeFailedCauseException;
 
   /*
    * Public API: constructor, init, synchronize.
    */
   public SynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) {
     this.setSynchronizer(synchronizer);
     this.delegate = delegate;
   }
@@ -109,39 +117,72 @@ implements RecordsChannelDelegate,
    * Valid only after first flow has completed.
    *
    * @return number of records, or -1 if not valid.
    */
   public int getInboundCount() {
     return numInboundRecords.get();
   }
 
+  public int getInboundCountStored() {
+    return numInboundRecordsStored.get();
+  }
+
+  public int getInboundCountFailed() {
+    return numInboundRecordsFailed.get();
+  }
+
+  public int getInboundCountReconciled() {
+    return numInboundRecordsReconciled.get();
+  }
+
   /**
    * Get the number of records fetched from the second repository (usually the
    * local store, hence outbound).
    * <p>
    * Valid only after second flow has completed.
    *
    * @return number of records, or -1 if not valid.
    */
   public int getOutboundCount() {
     return numOutboundRecords.get();
   }
 
+  public int getOutboundCountStored() {
+    return numOutboundRecordsStored.get();
+  }
+
+  public int getOutboundCountFailed() {
+    return numOutboundRecordsFailed.get();
+  }
+
+  public Exception getFetchFailedCauseException() {
+    return fetchFailedCauseException;
+  }
+
+  public Exception getStoreFailedCauseException() {
+    return storeFailedCauseException;
+  }
+
   // These are accessed by `abort` and `synchronize`, both of which are synchronized.
   // Guarded by `this`.
   protected RecordsChannel channelAToB;
   protected RecordsChannel channelBToA;
 
   /**
    * Please don't call this until you've been notified with onInitialized.
    */
   public synchronized void synchronize() {
     numInboundRecords.set(-1);
+    numInboundRecordsStored.set(-1);
+    numInboundRecordsFailed.set(-1);
+    numInboundRecordsReconciled.set(-1);
     numOutboundRecords.set(-1);
+    numOutboundRecordsStored.set(-1);
+    numOutboundRecordsFailed.set(-1);
 
     // First thing: decide whether we should.
     if (sessionA.shouldSkip() ||
         sessionB.shouldSkip()) {
       Logger.info(LOG_TAG, "Session requested skip. Short-circuiting sync.");
       sessionA.abort();
       sessionB.abort();
       this.delegate.onSynchronizeSkipped(this);
@@ -167,21 +208,26 @@ implements RecordsChannelDelegate,
       public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) {
         Logger.warn(LOG_TAG, "First RecordsChannel onFlowBeginFailed. Logging session error.", ex);
         session.delegate.onSynchronizeFailed(session, ex, "Failed to begin first flow.");
       }
 
       @Override
       public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) {
         Logger.warn(LOG_TAG, "First RecordsChannel onFlowFetchFailed. Logging remote fetch error.", ex);
+        fetchFailedCauseException = ex;
       }
 
       @Override
       public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) {
         Logger.warn(LOG_TAG, "First RecordsChannel onFlowStoreFailed. Logging local store error.", ex);
+        // Currently we're just recording the very last exception which occurred. This is a reasonable
+        // approach, but ideally we'd want to categorize the exceptions and count them for the purposes
+        // of better telemetry. See Bug 1362208.
+        storeFailedCauseException = ex;
       }
 
       @Override
       public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) {
         Logger.warn(LOG_TAG, "First RecordsChannel onFlowFinishedFailed. Logging session error.", ex);
         session.delegate.onSynchronizeFailed(session, ex, "Failed to finish first flow.");
       }
     };
@@ -206,16 +252,19 @@ implements RecordsChannelDelegate,
    * @param storeEnd timestamp when stores completed.
    */
   public void onFirstFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
     Logger.trace(LOG_TAG, "First RecordsChannel onFlowCompleted.");
     Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Starting next.");
     pendingATimestamp = fetchEnd;
     storeEndBTimestamp = storeEnd;
     numInboundRecords.set(recordsChannel.getFetchCount());
+    numInboundRecordsStored.set(recordsChannel.getStoreAcceptedCount());
+    numInboundRecordsFailed.set(recordsChannel.getStoreFailureCount());
+    numInboundRecordsReconciled.set(recordsChannel.getStoreReconciledCount());
     flowAToBCompleted = true;
     channelBToA.flow();
   }
 
   /**
    * Called after the second flow completes.
    * <p>
    * By default, any fetch and store failures are ignored.
@@ -225,16 +274,18 @@ implements RecordsChannelDelegate,
    */
   public void onSecondFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
     Logger.trace(LOG_TAG, "Second RecordsChannel onFlowCompleted.");
     Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Finishing.");
 
     pendingBTimestamp = fetchEnd;
     storeEndATimestamp = storeEnd;
     numOutboundRecords.set(recordsChannel.getFetchCount());
+    numOutboundRecordsStored.set(recordsChannel.getStoreAcceptedCount());
+    numOutboundRecordsFailed.set(recordsChannel.getStoreFailureCount());
     flowBToACompleted = true;
 
     // Finish the two sessions.
     try {
       this.sessionA.finish(this);
     } catch (InactiveSessionException e) {
       this.onFinishFailed(e);
       return;
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryCollector.java
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.telemetry;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import org.mozilla.gecko.sync.CollectionConcurrentModificationException;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.SyncDeadlineReachedException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Gathers telemetry about a single run of sync.
+ * In light of sync restarts, rarely a "single sync" will actually include more than one sync.
+ * See {@link TelemetryStageCollector} for "stage telemetry".
+ *
+ * @author grisha
+ */
+public class TelemetryCollector {
+    private static final String LOG_TAG = "TelemetryCollector";
+
+    public static final String KEY_ERROR_INTERNAL = "internal";
+    public static final String KEY_ERROR_TOKEN = "token";
+
+    // Telemetry collected by individual stages is aggregated here. Stages run sequentially,
+    // and only access their own collectors.
+    private final HashMap<String, TelemetryStageCollector> stageCollectors = new HashMap<>();
+
+    // Data which is not specific to a single stage is aggregated in this object.
+    @VisibleForTesting protected ExtendedJSONObject error;
+    private String hashedUID;
+    private String hashedDeviceID;
+    private final ArrayList<Bundle> devices = new ArrayList<>();
+
+    @Nullable private Long started;
+    @Nullable private Long finished;
+
+    private boolean didRestart = false;
+
+    public TelemetryStageCollector collectorFor(@NonNull String stageName) {
+        if (stageCollectors.containsKey(stageName)) {
+            return stageCollectors.get(stageName);
+        }
+
+        final TelemetryStageCollector collector = new TelemetryStageCollector(this);
+        stageCollectors.put(stageName, collector);
+        return collector;
+    }
+
+    public void setRestarted() {
+        this.didRestart = true;
+    }
+
+    public void setIDs(@NonNull String uid, @NonNull String deviceID) {
+        // We use hashed_fxa_uid from the token server as our UID.
+        this.hashedUID = uid;
+        try {
+            this.hashedDeviceID = Utils.byte2Hex(Utils.sha256(
+                            deviceID.concat(uid).getBytes("UTF-8")
+                    ));
+        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+            // Should not happen.
+            Log.e(LOG_TAG, "Either UTF-8 or SHA-256 are not supported", e);
+        }
+    }
+
+    public void setError(@NonNull String name, @NonNull String details) {
+        final ExtendedJSONObject error = new ExtendedJSONObject();
+        error.put("name", name);
+        error.put("error", details);
+        this.error = error;
+    }
+
+    public void setStarted(long time) {
+        this.started = time;
+    }
+
+    public void setFinished(long time) {
+        this.finished = time;
+    }
+
+    // In the current sync-ping parlance, "device" is really just a sync client.
+    // At some point we will start recording actual FxA devices.
+    public void addDevice(final ClientRecord client) {
+        if (this.hashedUID == null) {
+            throw new IllegalStateException("Must call setIDs before adding devices.");
+        }
+
+        final Bundle device = new Bundle();
+        device.putString(TelemetryContract.KEY_DEVICE_OS, client.os);
+        device.putString(TelemetryContract.KEY_DEVICE_VERSION, client.version);
+
+        final String clientAndUid = client.guid.concat(this.hashedUID);
+        try {
+            device.putString(
+                    TelemetryContract.KEY_DEVICE_ID,
+                    Utils.byte2Hex(Utils.sha256(clientAndUid.getBytes("UTF-8")))
+            );
+        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+            // Should not happen.
+            Log.e(LOG_TAG, "Either UTF-8 or SHA-256 are not supported", e);
+        }
+        devices.add(device);
+    }
+
+    public Bundle build() {
+        if (this.started == null) {
+            throw new IllegalStateException("Telemetry missing 'started' timestamp");
+        }
+        if (this.finished == null) {
+            throw new IllegalStateException("Telemetry missing 'finished' timestamp");
+        }
+
+        final long took = this.finished - this.started;
+
+        final Bundle telemetry = new Bundle();
+        telemetry.putString(TelemetryContract.KEY_LOCAL_UID, this.hashedUID);
+        telemetry.putString(TelemetryContract.KEY_LOCAL_DEVICE_ID, this.hashedDeviceID);
+        telemetry.putParcelableArrayList(TelemetryContract.KEY_DEVICES, this.devices);
+        telemetry.putLong(TelemetryContract.KEY_TOOK, took);
+        telemetry.putSerializable(TelemetryContract.KEY_ERROR, (Serializable) this.error);
+        telemetry.putSerializable(TelemetryContract.KEY_STAGES, this.stageCollectors);
+        if (this.didRestart) {
+            telemetry.putBoolean(TelemetryContract.KEY_RESTARTED, true);
+        }
+        return telemetry;
+    }
+
+    /**
+     * Builder class which is responsible for mapping instances of exceptions thrown during sync
+     * stages into a JSON structure that may be submitted as part of a sync ping.
+     */
+    public static class StageErrorBuilder {
+        @Nullable private Exception lastException;
+        @Nullable Exception storeException;
+        @Nullable Exception fetchException;
+
+        @Nullable private final String name;
+        @Nullable private final String error;
+
+        public StageErrorBuilder() {
+            this(null, null);
+        }
+
+        public StageErrorBuilder(@Nullable String name, @Nullable String error) {
+            this.name = name;
+            this.error = error;
+        }
+
+        public StageErrorBuilder setLastException(Exception e) {
+            lastException = e;
+            return this;
+        }
+
+        public StageErrorBuilder setStoreException(Exception e) {
+            storeException = e;
+            return this;
+        }
+
+        public StageErrorBuilder setFetchException(Exception e) {
+            fetchException = e;
+            return this;
+        }
+
+        // Unlike the rest of TelemetryCollector, which only vaguely hints at the particulars of a
+        // sync ping, this method contains specific details - naming of keys/values, etc.
+        // This is done consciously and for simplicity's sake. The alternative is to either pack
+        // these key/values behind an interface and unpack them on the receiver end, or let the receiver
+        // figure out how to deal with exceptions directly. Either way, we'll have a strong coupling.
+        public ExtendedJSONObject build() {
+            final ExtendedJSONObject errorJSON = new ExtendedJSONObject();
+
+            // Process manually set name, error and optional exception.
+            if (name != null && error != null) {
+                errorJSON.put("name", name);
+                errorJSON.put("error", error);
+
+                if (lastException != null && lastException instanceof HTTPFailureException) {
+                    final SyncStorageResponse response = ((HTTPFailureException)lastException).response;
+                    errorJSON.put("code", response.getStatusCode());
+                }
+
+                return errorJSON;
+            }
+
+            // Process set exceptions.
+            if (lastException instanceof CollectionConcurrentModificationException) {
+                errorJSON.put("name", "httperror");
+                errorJSON.put("code", 412);
+
+            } else if (lastException instanceof SyncDeadlineReachedException) {
+                errorJSON.put("name", "unexpected");
+                errorJSON.put("error", "syncdeadline");
+
+            } else if (lastException instanceof FetchFailedException) {
+                if (isNetworkError(fetchException)) {
+                    errorJSON.put("name", "networkerror");
+                    errorJSON.put("error", "fetch:" + fetchException.getClass().getSimpleName());
+                } else {
+                    errorJSON.put("name", "othererror");
+                    if (fetchException != null) {
+                        errorJSON.put("error", "fetch:" + fetchException.getClass().getSimpleName());
+                    } else {
+                        errorJSON.put("error", "fetch:unknown");
+                    }
+                }
+
+            } else if (lastException instanceof StoreFailedException) {
+                if (isNetworkError(storeException)) {
+                    errorJSON.put("name", "networkerror");
+                    errorJSON.put("error", "store:" + storeException.getClass().getSimpleName());
+
+                // Currently we only have access to one exception, the last one that happened. However, there
+                // could have been multiple errors that we're currently ignoring. See Bug 1362208.
+                // Local store failures are ignored (but will get recorded in the incoming failure count).
+                // Remote store failures generally do not abort the session, but will bubble up as an error.
+                // See Bug 1362206.
+                } else {
+                    errorJSON.put("name", "othererror");
+                    if (storeException != null) {
+                        errorJSON.put("error", "store:" + storeException.getClass().getSimpleName());
+                    } else {
+                        errorJSON.put("error", "store:unknown");
+                    }
+                }
+
+            } else if (lastException instanceof HTTPFailureException) {
+                final SyncStorageResponse response = ((HTTPFailureException)lastException).response;
+
+                // Is it an auth error? This could be a password change or a node re-assignment, and
+                // we can't distinguish between the two until we fetch new cluster URL during the
+                // next sync session.
+                if (response.getStatusCode() == 401) {
+                    errorJSON.put("name", "autherror");
+                    // Desktop clients differentiate between "tokenserver", "hawkclient", and "fxaccounts".
+                    // We will encounter this error during a sync stage run, and so it will come
+                    // from a sync storage node.
+                    errorJSON.put("from", "storage");
+                } else {
+                    errorJSON.put("name", "httperror");
+                    errorJSON.put("code", response.getStatusCode());
+                }
+
+            } else if (lastException != null) {
+                errorJSON.put("name", "unexpected");
+                errorJSON.put("error", lastException.getClass().getSimpleName());
+            }
+
+            return errorJSON;
+        }
+
+        private static boolean isNetworkError(@Nullable Exception e) {
+            return e instanceof java.net.SocketException;
+        }
+    }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java
@@ -1,56 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.telemetry;
 
+/**
+ * Establishes a common interface between {@link TelemetryCollector} and
+ * {@link org.mozilla.gecko.telemetry.TelemetryBackgroundReceiver}.
+ */
 public class TelemetryContract {
-  /**
-   * We are a Sync 1.1 (legacy) client, and we downloaded a migration sentinel.
-   */
-  public static final String SYNC11_MIGRATION_SENTINELS_SEEN = "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN";
-
-  /**
-   * We are a Sync 1.1 (legacy) client and we have downloaded a migration
-   * sentinel, but there was an error creating a Firefox Account from that
-   * sentinel.
-   * <p>
-   * We have logged the error and are ignoring that sentinel.
-   */
-  public static final String SYNC11_MIGRATIONS_FAILED = "FENNEC_SYNC11_MIGRATIONS_FAILED";
-
-  /**
-   * We are a Sync 1.1 (legacy) client and we have downloaded a migration
-   * sentinel, and there was no reported error creating a Firefox Account from
-   * that sentinel.
-   * <p>
-   * We have created a Firefox Account corresponding to the sentinel and have
-   * queued the existing Old Sync account for removal.
-   */
-  public static final String SYNC11_MIGRATIONS_SUCCEEDED = "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED";
+  public final static String KEY_TELEMETRY = "telemetry";
+  public final static String KEY_STAGES = "stages";
+  public final static String KEY_ERROR = "error";
+  public final static String KEY_LOCAL_UID = "uid";
+  public final static String KEY_LOCAL_DEVICE_ID = "deviceID";
+  public final static String KEY_DEVICES = "devices";
+  public final static String KEY_TOOK = "took";
+  public final static String KEY_RESTARTED = "restarted";
 
-  /**
-   * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from
-   * Sync 1.1. We have presented the user the "complete upgrade" notification.
-   * <p>
-   * We will offer every time a sync is triggered, including when a notification
-   * is already pending.
-   */
-  public static final String SYNC11_MIGRATION_NOTIFICATIONS_OFFERED = "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED";
+  public static final String KEY_TYPE = "type";
+  public static final String KEY_TYPE_SYNC = "sync";
+  public static final String KEY_TYPE_EVENT = "event";
 
-  /**
-   * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from
-   * Sync 1.1. We have presented the user the "complete upgrade" notification
-   * and they have successfully completed the upgrade process by entering their
-   * Firefox Account credentials.
-   */
-  public static final String SYNC11_MIGRATIONS_COMPLETED = "FENNEC_SYNC11_MIGRATIONS_COMPLETED";
-
-  public static final String SYNC_STARTED = "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED";
-
-  public static final String SYNC_COMPLETED = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED";
-
-  public static final String SYNC_FAILED = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED";
-
-  public static final String SYNC_FAILED_BACKOFF = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF";
+  public static final String KEY_DEVICE_OS = "os";
+  public static final String KEY_DEVICE_VERSION = "version";
+  public static final String KEY_DEVICE_ID = "id";
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryStageCollector.java
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.telemetry;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * Gathers telemetry details about an individual sync stage.
+ * Implementation note: there are no getters/setters to avoid unnecessary verboseness.
+ * This data expected to be write-only from within SyncStages, and read-only from TelemetryCollector.
+ * Although there shouldn't be concurrent access, it's possible that we'll be reading/writing these
+ * values from different threads - hence `volatile` to ensure visibility.
+ */
+public class TelemetryStageCollector {
+    private final TelemetryCollector syncCollector;
+
+    public volatile long started = 0L;
+    public volatile long finished = 0L;
+    public volatile int inbound = 0;
+    public volatile int inboundStored = 0;
+    public volatile int inboundFailed = 0;
+    public volatile int outbound = 0;
+    public volatile int outboundStored = 0;
+    public volatile int outboundFailed = 0;
+    public volatile int reconciled = 0;
+    public volatile ExtendedJSONObject error = null;
+
+    public TelemetryStageCollector(TelemetryCollector syncCollector) {
+        this.syncCollector = syncCollector;
+    }
+
+    public TelemetryCollector getSyncCollector() {
+        return this.syncCollector;
+    }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
@@ -46,26 +46,26 @@ import ch.boye.httpclientandroidlib.mess
  * authorization credential. Usually, it used to exchange a public-key
  * authorization token that is expensive to validate for a symmetric-key
  * authorization that is cheap to validate. For example, we might exchange a
  * BrowserID assertion for a HAWK id and key pair.
  */
 public class TokenServerClient {
   protected static final String LOG_TAG = "TokenServerClient";
 
-  public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
-  public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
-  public static final String JSON_KEY_DURATION = "duration";
-  public static final String JSON_KEY_ERRORS = "errors";
-  public static final String JSON_KEY_ID = "id";
-  public static final String JSON_KEY_KEY = "key";
-  public static final String JSON_KEY_UID = "uid";
+  private static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
+  private static final String JSON_KEY_CONDITION_URLS = "condition_urls";
+  private static final String JSON_KEY_ERRORS = "errors";
+  private static final String JSON_KEY_ID = "id";
+  private static final String JSON_KEY_KEY = "key";
+  private static final String JSON_KEY_UID = "uid";
+  private static final String JSON_KEY_HASHED_FXA_UID = "hashed_fxa_uid";
 
-  public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
-  public static final String HEADER_CLIENT_STATE = "X-Client-State";
+  private static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
+  private static final String HEADER_CLIENT_STATE = "X-Client-State";
 
   protected final Executor executor;
   protected final URI uri;
 
   public TokenServerClient(URI uri, Executor executor) {
     if (uri == null) {
       throw new IllegalArgumentException("uri must not be null");
     }
@@ -215,27 +215,28 @@ public class TokenServerClient {
         throw new TokenServerUnknownServiceException(errorList);
       }
 
       // We shouldn't ever get here...
       throw new TokenServerException(errorList);
     }
 
     try {
-      result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT }, String.class);
+      result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT, JSON_KEY_HASHED_FXA_UID }, String.class);
       result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_UID }, Long.class);
     } catch (BadRequiredFieldJSONException e ) {
       throw new TokenServerMalformedResponseException(null, e);
     }
 
     Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID));
 
     return new TokenServerToken(result.getString(JSON_KEY_ID),
         result.getString(JSON_KEY_KEY),
         result.get(JSON_KEY_UID).toString(),
+        result.getString(JSON_KEY_HASHED_FXA_UID),
         result.getString(JSON_KEY_API_ENDPOINT));
   }
 
   public static class TokenFetchResourceDelegate extends BaseResourceDelegate {
     private final TokenServerClient         client;
     private final TokenServerClientDelegate delegate;
     private final String                    assertion;
     private final String                    clientState;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java
@@ -3,17 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tokenserver;
 
 public class TokenServerToken {
   public final String id;
   public final String key;
   public final String uid;
+  public final String hashedFxaUid;
   public final String endpoint;
 
-  public TokenServerToken(String id, String key, String uid, String endpoint) {
+  public TokenServerToken(String id, String key, String uid, String hashedFxaUid, String endpoint) {
     this.id = id;
     this.key = key;
     this.uid = uid;
+    this.hashedFxaUid = hashedFxaUid;
     this.endpoint = endpoint;
   }
 }
\ No newline at end of file
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java
@@ -743,16 +743,20 @@ public class TestBookmarks extends Andro
             finishAndNotify(session);
           }
 
           @Override
           public void onRecordStoreSucceeded(String guid) {
           }
 
           @Override
+          public void onRecordStoreReconciled(String guid) {
+          }
+
+          @Override
           public void onStoreFailed(Exception e) {
 
           }
         };
         session.setStoreDelegate(storeDelegate);
         for (BookmarkRecord record : records) {
           try {
             session.store(record);
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java
@@ -13,16 +13,17 @@ import org.mozilla.gecko.sync.SyncConfig
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 
 public class TestClientsStage extends AndroidSyncTestCase {
   private static final String TEST_USERNAME    = "johndoe";
   private static final String TEST_PASSWORD    = "password";
   private static final String TEST_SYNC_KEY    = "abcdeabcdeabcdeabcdeabcdea";
@@ -45,17 +46,17 @@ public class TestClientsStage extends An
     final GlobalSessionCallback callback = new DefaultGlobalSessionCallback();
     final ClientsDataDelegate delegate = new MockClientsDataDelegate();
 
     final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
     final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
     final SharedPreferences prefs = new MockSharedPreferences();
     final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
     config.syncKeyBundle = keyBundle;
-    GlobalSession session = new GlobalSession(config, callback, context, delegate);
+    GlobalSession session = new GlobalSession(config, callback, context, delegate, new TelemetryCollector());
 
     SyncClientsEngineStage stage = new SyncClientsEngineStage() {
 
       @Override
       public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
         if (db == null) {
           db = dataAccessor;
         }
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
@@ -19,16 +19,18 @@ import org.mozilla.gecko.sync.SyncConfig
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
 
 /**
  * Test the on-device side effects of reset operations on a stage.
  *
  * See also "TestResetCommands" in the unit test suite.
  */
 public class TestResetting extends AndroidSyncTestCase {
   private static final String TEST_USERNAME    = "johndoe";
@@ -136,32 +138,32 @@ public class TestResetting extends Andro
      * Run this stage synchronously.
      */
     public void executeSynchronously(final GlobalSession session) {
       final BaseMockServerSyncStage self = this;
       performWait(new Runnable() {
         @Override
         public void run() {
           try {
-            self.execute(session);
+            self.execute(session, new TelemetryStageCollector(new TelemetryCollector()));
           } catch (NoSuchStageException e) {
             performNotify(e);
           }
         }
       });
     }
   }
 
   private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws Exception {
     final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
     final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
     final SharedPreferences prefs = new MockSharedPreferences();
     final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
     config.syncKeyBundle = keyBundle;
-    return new GlobalSession(config, callback, getApplicationContext(), null) {
+    return new GlobalSession(config, callback, getApplicationContext(), null, new TelemetryCollector()) {
       @Override
       public boolean isEngineRemotelyEnabled(String engineName,
                                      EngineSettings engineSettings)
         throws MetaGlobalException {
         return true;
       }
 
       @Override
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java
@@ -56,16 +56,22 @@ public class TestStoreTracking extends A
 
       @Override
       public void onRecordStoreSucceeded(String guid) {
         Logger.debug(getName(), "Stored " + guid);
         assertEq(expectedGUID, guid);
       }
 
       @Override
+      public void onRecordStoreReconciled(String guid) {
+        Logger.debug(getName(), "Reconciled " + guid);
+        assertEq(expectedGUID, guid);
+      }
+
+      @Override
       public void onStoreCompleted(long storeEnd) {
         Logger.debug(getName(), "Store completed at " + storeEnd + ".");
         try {
           session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() {
             @Override
             public void onFetchedRecord(Record record) {
               Logger.debug(getName(), "Hurrah! Fetched record " + record.guid);
               assertEq(expectedGUID, record.guid);
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
@@ -25,16 +25,19 @@ public class DefaultStoreDelegate extend
   }
 
   @Override
   public void onStoreFailed(Exception ex) {
     performNotify("Store failed", ex);
   }
 
   @Override
+  public void onRecordStoreReconciled(String guid) {}
+
+  @Override
   public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) {
     final RepositorySessionStoreDelegate self = this;
     return new RepositorySessionStoreDelegate() {
 
       @Override
       public void onRecordStoreSucceeded(final String guid) {
         executor.execute(new Runnable() {
           @Override
@@ -50,16 +53,26 @@ public class DefaultStoreDelegate extend
           @Override
           public void run() {
             self.onRecordStoreFailed(ex, guid);
           }
         });
       }
 
       @Override
+      public void onRecordStoreReconciled(final String guid) {
+        executor.execute(new Runnable() {
+          @Override
+          public void run() {
+            self.onRecordStoreReconciled(guid);
+          }
+        });
+      }
+
+      @Override
       public void onStoreCompleted(final long storeEnd) {
         executor.execute(new Runnable() {
           @Override
           public void run() {
             self.onStoreCompleted(storeEnd);
           }
         });
       }
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
@@ -43,16 +43,20 @@ public class DefaultGlobalSessionCallbac
   public void handleAborted(GlobalSession globalSession, String reason) {
   }
 
   @Override
   public void handleError(GlobalSession globalSession, Exception ex) {
   }
 
   @Override
+  public void handleError(GlobalSession globalSession, Exception ex, String reason) {
+  }
+
+  @Override
   public void handleSuccess(GlobalSession globalSession) {
   }
 
   @Override
   public void handleStageCompleted(Stage currentState,
                                    GlobalSession globalSession) {
   }
 
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -10,32 +10,33 @@ import org.mozilla.gecko.sync.GlobalSess
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import java.io.IOException;
 
 /**
  * GlobalSession touches the Android prefs system. Stub that out.
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
   public MockPrefsGlobalSession(
       SyncConfiguration config, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
       NonObjectJSONException {
-    super(config, callback, context, clientsDelegate);
+    super(config, callback, context, clientsDelegate, new TelemetryCollector());
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, String password,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
       NonObjectJSONException {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -35,16 +35,18 @@ import org.mozilla.gecko.sync.crypto.Key
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
+import org.mozilla.gecko.sync.telemetry.TelemetryStageCollector;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
 import java.io.PrintStream;
 import java.math.BigDecimal;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -67,16 +69,18 @@ import static org.junit.Assert.fail;
  */
 @RunWith(TestRunner.class)
 public class TestClientsEngineStage extends MockSyncClientsEngineStage {
   public final static String LOG_TAG = "TestClientsEngSta";
 
   public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
     super();
     session = initializeSession();
+    telemetryStageCollector = new TelemetryStageCollector(new TelemetryCollector());
+    telemetryStageCollector.getSyncCollector().setIDs("mockUID", "mockDeviceID");
   }
 
   // Static so we can set it during the constructor. This is so evil.
   private static MockGlobalSessionCallback callback;
   private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
     callback = new MockGlobalSessionCallback();
     SyncConfiguration config = new SyncConfiguration(USERNAME, new BasicAuthHeaderProvider(USERNAME, PASSWORD), new MockSharedPreferences());
     config.syncKeyBundle = new KeyBundle(USERNAME, SYNC_KEY);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
@@ -198,46 +198,46 @@ public class TestRecordsChannel {
     sinkRepository = empty();
     doFlow();
     assertEquals(1, numFlowCompleted.get());
     assertEquals(0, numFlowFetchFailed.get());
     assertEquals(0, numFlowStoreFailed.get());
     assertEquals(sourceRepository.wbos, sinkRepository.wbos);
     assertEquals(0, recordsChannel.getFetchFailureCount());
     assertEquals(0, recordsChannel.getStoreFailureCount());
-    assertEquals(6, recordsChannel.getStoreCount());
+    assertEquals(6, recordsChannel.getStoreAttemptedCount());
   }
 
   @Test
   public void testFetchFail() throws Exception {
     sourceRepository = failingFetch(SynchronizerHelpers.FailMode.FETCH);
     sinkRepository = empty();
     doFlow();
     assertEquals(1, numFlowCompleted.get());
     assertTrue(numFlowFetchFailed.get() > 0);
     assertEquals(0, numFlowStoreFailed.get());
     assertTrue(sinkRepository.wbos.size() < 6);
     assertTrue(recordsChannel.getFetchFailureCount() > 0);
     assertEquals(0, recordsChannel.getStoreFailureCount());
-    assertTrue(recordsChannel.getStoreCount() < 6);
+    assertTrue(recordsChannel.getStoreAttemptedCount() < 6);
   }
 
   @Test
   public void testStoreFetchFailedCollectionModified() throws Exception {
     sourceRepository = failingFetch(SynchronizerHelpers.FailMode.COLLECTION_MODIFIED);
     sinkRepository = empty();
     doFlow();
     assertEquals(1, numFlowCompleted.get());
     assertTrue(numFlowFetchFailed.get() > 0);
     assertEquals(0, numFlowStoreFailed.get());
     assertTrue(sinkRepository.wbos.size() < 6);
 
     assertTrue(recordsChannel.getFetchFailureCount() > 0);
     assertEquals(0, recordsChannel.getStoreFailureCount());
-    assertTrue(recordsChannel.getStoreCount() < sourceRepository.wbos.size());
+    assertTrue(recordsChannel.getStoreAttemptedCount() < sourceRepository.wbos.size());
 
     assertEquals(CollectionConcurrentModificationException.class, fetchException.getClass());
     final Exception ex = recordsChannel.getReflowException();
     assertNotNull(ex);
     assertEquals(CollectionConcurrentModificationException.class, ex.getClass());
   }
 
   @Test
@@ -247,17 +247,17 @@ public class TestRecordsChannel {
     doFlow();
     assertEquals(1, numFlowCompleted.get());
     assertTrue(numFlowFetchFailed.get() > 0);
     assertEquals(0, numFlowStoreFailed.get());
     assertTrue(sinkRepository.wbos.size() < 6);
 
     assertTrue(recordsChannel.getFetchFailureCount() > 0);
     assertEquals(0, recordsChannel.getStoreFailureCount());
-    assertTrue(recordsChannel.getStoreCount() < sourceRepository.wbos.size());
+    assertTrue(recordsChannel.getStoreAttemptedCount() < sourceRepository.wbos.size());
 
     assertEquals(SyncDeadlineReachedException.class, fetchException.getClass());
     final Exception ex = recordsChannel.getReflowException();
     assertNotNull(ex);
     assertEquals(SyncDeadlineReachedException.class, ex.getClass());
   }
 
   @Test
@@ -270,17 +270,17 @@ public class TestRecordsChannel {
     assertEquals(0, numFlowFetchFailed.get());
     assertEquals(1, numFlowStoreFailed.get());
     // We will fail to store one of the records but expect flow to continue.
     assertEquals(5, sinkRepository.wbos.size());
 
     assertEquals(0, recordsChannel.getFetchFailureCount());
     assertEquals(1, recordsChannel.getStoreFailureCount());
     // Number of store attempts.
-    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreCount());
+    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreAttemptedCount());
   }
 
   @Test
   public void testStoreSerialFailCollectionModified() throws Exception {
     sourceRepository = full();
     sinkRepository = new SynchronizerHelpers.SerialFailStoreWBORepository(
             SynchronizerHelpers.FailMode.COLLECTION_MODIFIED);
     doFlow();
@@ -309,28 +309,28 @@ public class TestRecordsChannel {
     assertEquals(1, numFlowCompleted.get());
     assertEquals(0, numFlowFetchFailed.get());
     assertEquals(3, numFlowStoreFailed.get()); // One batch fails.
     assertEquals(3, sinkRepository.wbos.size()); // One batch succeeds.
 
     assertEquals(0, recordsChannel.getFetchFailureCount());
     assertEquals(3, recordsChannel.getStoreFailureCount());
     // Number of store attempts.
-    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreCount());
+    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreAttemptedCount());
   }
 
 
   @Test
   public void testStoreOneBigBatchFail() throws Exception {
     sourceRepository = full();
     sinkRepository = new SynchronizerHelpers.BatchFailStoreWBORepository(50);
     doFlow();
     assertEquals(1, numFlowCompleted.get());
     assertEquals(0, numFlowFetchFailed.get());
     assertEquals(6, numFlowStoreFailed.get()); // One (big) batch fails.
     assertEquals(0, sinkRepository.wbos.size()); // No batches succeed.
 
     assertEquals(0, recordsChannel.getFetchFailureCount());
     assertEquals(6, recordsChannel.getStoreFailureCount());
     // Number of store attempts.
-    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreCount());
+    assertEquals(sourceRepository.wbos.size(), recordsChannel.getStoreAttemptedCount());
   }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
@@ -34,12 +34,17 @@ public class ExpectSuccessRepositorySess
 
   @Override
   public void onStoreFailed(Exception e) {
     log("Store failed.", e);
     performNotify(new AssertionFailedError("onStoreFailed: store should not have failed."));
   }
 
   @Override
+  public void onRecordStoreReconciled(String guid) {
+    log("Store reconciled record " + guid);
+  }
+
+  @Override
   public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
     return this;
   }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
@@ -48,16 +48,21 @@ public class MockGlobalSessionCallback i
 
   @Override
   public void handleAborted(GlobalSession globalSession, String reason) {
     this.calledAborted = true;
     this.testWaiter().performNotify();
   }
 
   @Override
+  public void handleError(GlobalSession globalSession, Exception ex, String reason) {
+    this.handleError(globalSession, ex);
+  }
+
+  @Override
   public void handleError(GlobalSession globalSession, Exception ex) {
     this.calledError = true;
     this.calledErrorException = ex;
     this.testWaiter().performNotify();
   }
 
   @Override
   public void handleIncompleteStage(Stage currentState,
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
@@ -27,16 +27,20 @@ public class DefaultGlobalSessionCallbac
   public void informMigrated(GlobalSession globalSession) {
   }
 
   @Override
   public void handleAborted(GlobalSession globalSession, String reason) {
   }
 
   @Override
+  public void handleError(GlobalSession globalSession, Exception ex, String reason) {
+  }
+
+  @Override
   public void handleError(GlobalSession globalSession, Exception ex) {
   }
 
   @Override
   public void handleSuccess(GlobalSession globalSession) {
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -10,31 +10,32 @@ import org.mozilla.gecko.sync.GlobalSess
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import java.io.IOException;
 
 /**
  * GlobalSession touches the Android prefs system. Stub that out.
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
   public MockPrefsGlobalSession(
       SyncConfiguration config, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
-    super(config, callback, context, clientsDelegate);
+    super(config, callback, context, clientsDelegate, new TelemetryCollector());
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, String password,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
     return getSession(username, new BasicAuthHeaderProvider(username, password), null,
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
@@ -149,16 +149,20 @@ public class BatchingUploaderTest {
         }
 
         @Override
         public void onStoreFailed(Exception e) {
             lastStoreFailedException = e;
         }
 
         @Override
+        public void onRecordStoreReconciled(String guid) {
+        }
+
+        @Override
         public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
             return this;
         }
     }
 
     private ExecutorService workQueue;
     private RepositorySessionStoreDelegate storeDelegate;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
@@ -107,16 +107,20 @@ public class PayloadUploadDelegateTest {
         }
 
         @Override
         public void onStoreFailed(Exception e) {
             storeFailedException = e;
         }
 
         @Override
+        public void onRecordStoreReconciled(String guid) {
+        }
+
+        @Override
         public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
             return null;
         }
     }
 
     @Before
     public void setUp() throws Exception {
         sessionStoreDelegate = new MockRepositorySessionStoreDelegate();
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/telemetry/TelemetryCollectorTest.java
@@ -0,0 +1,326 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.telemetry;
+
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CollectionConcurrentModificationException;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.SyncDeadlineReachedException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import java.util.ArrayList;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.doReturn;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class TelemetryCollectorTest {
+    private TelemetryCollector collector;
+
+    @Before
+    public void setUp() throws Exception {
+        collector = new TelemetryCollector();
+    }
+
+    @Test
+    public void testSetIDs() throws Exception {
+        final String deviceID = "random-device-guid";
+        final String uid = "already-hashed-test-uid";
+
+        collector.setIDs(uid, deviceID);
+        collector.setStarted(5L);
+        collector.setFinished(10L);
+        final Bundle bundle = collector.build();
+
+        // Setting UID is a pass-through, since we're expecting it to be hashed already.
+        assertEquals(uid, bundle.get("uid"));
+        // Expect device ID to be hashed with the UID.
+        assertEquals(
+                Utils.byte2Hex(Utils.sha256(deviceID.concat(uid).getBytes("UTF-8"))),
+                bundle.get("deviceID")
+        );
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testAddingDevicesBeforeSettingUID() throws Exception {
+        collector.addDevice(new ClientRecord("client1"));
+    }
+
+    @Test
+    public void testAddingDevices() throws Exception {
+        ClientRecord client1 = new ClientRecord("client1-guid");
+        client1.os = "iOS";
+        client1.version = "1.33.7";
+
+        collector.setStarted(5L);
+        collector.setFinished(10L);
+
+        collector.setIDs("hashed-uid", "deviceID");
+
+        // Test that client device ID is hashed together with UID
+        collector.addDevice(client1);
+
+        Bundle data = collector.build();
+        ArrayList<Bundle> devices = data.getParcelableArrayList("devices");
+        assertEquals(1, devices.size());
+        assertEquals(
+                Utils.byte2Hex(Utils.sha256("client1-guid".concat("hashed-uid").getBytes("UTF-8"))),
+                devices.get(0).getString("id")
+        );
+        assertEquals("iOS", devices.get(0).getString("os"));
+        assertEquals("1.33.7", devices.get(0).getString("version"));
+
+        // Test that we can add more than just one device
+        ClientRecord client2 = new ClientRecord("client2-guid");
+        client2.os = "Android";
+        client2.version = "55.0a1";
+        collector.addDevice(client2);
+
+        data = collector.build();
+        devices = data.getParcelableArrayList("devices");
+        assertEquals(2, devices.size());
+
+        assertEquals("iOS", devices.get(0).getString("os"));
+        assertEquals("1.33.7", devices.get(0).getString("version"));
+        assertEquals(
+                Utils.byte2Hex(Utils.sha256("client1-guid".concat("hashed-uid").getBytes("UTF-8"))),
+                devices.get(0).getString("id")
+        );
+
+        assertEquals("Android", devices.get(1).getString("os"));
+        assertEquals("55.0a1", devices.get(1).getString("version"));
+        assertEquals(
+                Utils.byte2Hex(Utils.sha256("client2-guid".concat("hashed-uid").getBytes("UTF-8"))),
+                devices.get(1).getString("id")
+        );
+    }
+
+    @Test
+    public void testDuration() throws Exception {
+        collector.setStarted(5L);
+        collector.setFinished(11L);
+        Bundle data = collector.build();
+
+        assertEquals(6L, data.getLong("took"));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testDurationMissingStarted() throws Exception {
+        collector.setFinished(10L);
+        collector.build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testDurationMissingFinished() throws Exception {
+        collector.setStarted(10L);
+        collector.build();
+    }
+
+    @Test
+    public void testError() throws Exception {
+        collector.setError("testError", "unexpectedStuff");
+        assertEquals("testError", collector.error.getString("name"));
+        assertEquals("unexpectedStuff", collector.error.getString("error"));
+    }
+
+    @Test
+    public void testRestarted() throws Exception {
+        collector.setStarted(5L);
+        collector.setFinished(10L);
+
+        Bundle data = collector.build();
+        assertFalse(data.getBoolean("restarted"));
+        assertFalse(data.containsKey("restarted"));
+
+        collector.setRestarted();
+
+        assertTrue(collector.build().getBoolean("restarted"));
+    }
+
+    @Test
+    public void testCollectorFor() throws Exception {
+        // Test that we'll get the same stage collector for the same stage name
+        TelemetryStageCollector stageCollector = collector.collectorFor("test");
+        TelemetryStageCollector stageCollector2 = collector.collectorFor("test");
+        assertEquals(stageCollector, stageCollector2);
+
+        // Test that we won't just keep getting the same stage collector for different stages
+        TelemetryStageCollector stageCollector3 = collector.collectorFor("another");
+        assertNotEquals(stageCollector, stageCollector3);
+    }
+
+    @Test
+    public void testStageErrorBuilder() throws Exception {
+        TelemetryCollector.StageErrorBuilder builder = new TelemetryCollector.StageErrorBuilder("test", "sampleerror");
+        ExtendedJSONObject error = builder.build();
+
+        // Test setting error name/details manually.
+        assertEquals("test", error.getString("name"));
+        assertEquals("sampleerror", error.getString("error"));
+
+        SyncStorageResponse response400 = mock(SyncStorageResponse.class);
+        doReturn(400).when(response400).getStatusCode();
+
+        // Test setting an optional HTTP exception while manually setting error/details.
+        builder.setLastException(new HTTPFailureException(response400));
+
+        error = builder.build();
+        assertEquals("test", error.getString("name"));
+        assertEquals("sampleerror", error.getString("error"));
+        assertEquals(Integer.valueOf(400), error.getIntegerSafely("code"));
+
+        // Test deadline reached errors
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(SyncDeadlineReachedException.class));
+
+        error = builder.build();
+        assertEquals("unexpected", error.getString("name"));
+        assertEquals("syncdeadline", error.getString("error"));
+
+        // Test that internal concurrent modification exceptions are treated as HTTP 412
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(CollectionConcurrentModificationException.class));
+
+        error = builder.build();
+        assertEquals("httperror", error.getString("name"));
+        assertEquals(Integer.valueOf(412), error.getIntegerSafely("code"));
+
+        // Test a non-401 HTTP exception
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(new HTTPFailureException(response400));
+
+        error = builder.build();
+        assertEquals("httperror", error.getString("name"));
+        assertEquals(Integer.valueOf(400), error.getIntegerSafely("code"));
+
+        // Test 401 HTTP exceptions
+        SyncStorageResponse response401 = mock(SyncStorageResponse.class);
+        doReturn(401).when(response401).getStatusCode();
+
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(new HTTPFailureException(response401));
+
+        error = builder.build();
+        assertEquals("autherror", error.getString("name"));
+        assertEquals("storage", error.getString("from"));
+
+        // Test generic exception handling
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(new NullPointerException());
+
+        error = builder.build();
+        assertEquals("unexpected", error.getString("name"));
+        assertEquals("NullPointerException", error.getString("error"));
+
+        // Test exceptions encountered during a fetch phase of a synchronizer
+        // Generic network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(FetchFailedException.class))
+                .setFetchException(new java.net.SocketException());
+
+        error = builder.build();
+        assertEquals("networkerror", error.getString("name"));
+        assertEquals("fetch:SocketException", error.getString("error"));
+
+        // Non-network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(FetchFailedException.class))
+                .setFetchException(new IllegalStateException());
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("fetch:IllegalStateException", error.getString("error"));
+
+        // Missing specific error
+        // Non-network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(FetchFailedException.class));
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("fetch:unknown", error.getString("error"));
+
+        // Test exceptions encountered during a store phase of a synchronizer
+        // Generic network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(StoreFailedException.class))
+                .setStoreException(new java.net.SocketException());
+
+        error = builder.build();
+        assertEquals("networkerror", error.getString("name"));
+        assertEquals("store:SocketException", error.getString("error"));
+
+        // Non-network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(StoreFailedException.class))
+                .setStoreException(new IllegalStateException());
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("store:IllegalStateException", error.getString("error"));
+
+        // Missing specific error
+        // Non-network error
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(StoreFailedException.class));
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("store:unknown", error.getString("error"));
+
+        // Test ability to blindly set both types of exceptions (store & fetch)
+        // one of them being null. Just as the ServerSyncStage does on failures.
+        // Store exceptions
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(StoreFailedException.class))
+                .setFetchException(null)
+                .setStoreException(new IllegalArgumentException());
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("store:IllegalArgumentException", error.getString("error"));
+
+        // Fetch exceptions
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(FetchFailedException.class))
+                .setFetchException(new java.net.SocketException())
+                .setStoreException(null);
+
+        error = builder.build();
+        assertEquals("networkerror", error.getString("name"));
+        assertEquals("fetch:SocketException", error.getString("error"));
+
+        // Both types, but indicating that last one was a fetch exception
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(FetchFailedException.class))
+                .setFetchException(new java.net.SocketException())
+                .setStoreException(new IllegalStateException());
+
+        error = builder.build();
+        assertEquals("networkerror", error.getString("name"));
+        assertEquals("fetch:SocketException", error.getString("error"));
+
+        // Both types, but indicating that last one was a store exception
+        builder = new TelemetryCollector.StageErrorBuilder();
+        builder.setLastException(mock(StoreFailedException.class))
+                .setFetchException(new java.net.SocketException())
+                .setStoreException(new IllegalStateException());
+
+        error = builder.build();
+        assertEquals("othererror", error.getString("name"));
+        assertEquals("store:IllegalStateException", error.getString("error"));
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TelemetrySyncPingBuilderTest.java
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.simple.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryLocalPing;
+
+import java.util.ArrayList;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class TelemetrySyncPingBuilderTest {
+    private TelemetrySyncPingBundleBuilderTest.MockTelemetryPingStore pingStore;
+    private TelemetrySyncPingBuilder builder;
+
+    @Before
+    public void setUp() throws Exception {
+        pingStore = new TelemetrySyncPingBundleBuilderTest.MockTelemetryPingStore();
+        builder = new TelemetrySyncPingBuilder();
+    }
+
+    @Test
+    public void testGeneralShape() throws Exception {
+        TelemetryLocalPing localPing = builder
+                .setDeviceID("device-id")
+                .setUID("uid")
+                .setTook(123L)
+                .setRestarted(false)
+                .build();
+        ExtendedJSONObject payload = localPing.getPayload();
+        assertEquals("uid", payload.getString("uid"));
+        assertEquals(Long.valueOf(123L), payload.getLong("took"));
+        assertEquals("device-id", payload.getString("deviceID"));
+        assertEquals(Integer.valueOf(1), payload.getIntegerSafely("version"));
+        assertFalse(payload.containsKey("restarted"));
+
+        localPing = builder
+                .setDeviceID("device-id")
+                .setUID("uid")
+                .setTook(123L)
+                .setRestarted(true)
+                .build();
+        payload = localPing.getPayload();
+        assertEquals("uid", payload.getString("uid"));
+        assertEquals(Long.valueOf(123L), payload.getLong("took"));
+        assertEquals("device-id", payload.getString("deviceID"));
+        assertEquals(Integer.valueOf(1), payload.getIntegerSafely("version"));
+        assertEquals(true, payload.getBoolean("restarted"));
+    }
+
+    @Test
+    public void testDevices() throws Exception {
+        ArrayList<Parcelable> devices = new ArrayList<>();
+
+        TelemetryLocalPing localPing = builder
+                .setDevices(devices)
+                .build();
+        ExtendedJSONObject payload = localPing.getPayload();
+
+        // Empty list isn't part of the JSON.
+        assertEquals(
+                "{\"version\":1}",
+                payload.toString()
+        );
+
+        Bundle device = new Bundle();
+        device.putString("os", "Android");
+        device.putString("version", "53.0a1");
+        device.putString("id", "80daf12dsadsa4236914cff2cc6e9d0f80a965380e2cf8e976e4004ead887521b5d9");
+        devices.add(device);
+
+        // Test with only one device
+        payload = builder
+                .setDevices(devices)
+                .build()
+                .getPayload();
+        JSONArray devicesJSON = payload.getArray("devices");
+        assertEquals(1, devicesJSON.size());
+        assertDevice((ExtendedJSONObject) devicesJSON.get(0), "Android", "53.0a1", "80daf12dsadsa4236914cff2cc6e9d0f80a965380e2cf8e976e4004ead887521b5d9");
+
+        device = new Bundle();
+        device.putString("os", "iOS");
+        device.putString("version", "8.0");
+        device.putString("id", "fa813452774b3cdc8f5f73290b5346df800f644b7b92a1ab94b6e2af748d261362");
+        devices.add(device);
+
+        // Test with more than one device
+        payload = builder
+                .setDevices(devices)
+                .build()
+                .getPayload();
+        devicesJSON = payload.getArray("devices");
+        assertEquals(2, devicesJSON.size());
+        assertDevice((ExtendedJSONObject) devicesJSON.get(0), "Android", "53.0a1", "80daf12dsadsa4236914cff2cc6e9d0f80a965380e2cf8e976e4004ead887521b5d9");
+        assertDevice((ExtendedJSONObject) devicesJSON.get(1), "iOS", "8.0", "fa813452774b3cdc8f5f73290b5346df800f644b7b92a1ab94b6e2af748d261362");
+    }
+
+    private void assertDevice(ExtendedJSONObject device, String os, String version, String id) throws JSONException {
+        assertEquals(os, device.getString("os"));
+        assertEquals(version, device.getString("version"));
+        assertEquals(id, device.getString("id"));
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TelemetrySyncPingBundleBuilderTest.java
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.telemetry.pingbuilders;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.json.JSONException;
+import org.json.simple.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
+import org.mozilla.gecko.telemetry.TelemetryPing;
+import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class TelemetrySyncPingBundleBuilderTest {
+    public static class MockTelemetryPingStore extends TelemetryPingStore {
+        public MockTelemetryPingStore() {
+            super("default");
+        }
+
+        // Stable ordering for the sake of easier testing.
+        private LinkedHashMap<String, TelemetryPing> pings = new LinkedHashMap<>();
+
+        @Override
+        public List<TelemetryPing> getAllPings() {
+            return new ArrayList<>(pings.values());
+        }
+
+        @Override
+        public int getCount() {
+            return pings.size();
+        }
+
+        @Override
+        public void storePing(TelemetryPing ping) throws IOException {
+            pings.put(ping.getDocID(), ping);
+        }
+
+        @Override
+        public void maybePrunePings() {}
+
+        @Override
+        public void onUploadAttemptComplete(Set<String> successfulRemoveIDs) {
+            for (String id : successfulRemoveIDs) {
+                pings.remove(id);
+            }
+        }
+
+        @Override
+        public Set<String> getStoredIDs() {
+            return pings.keySet();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+
+        }
+
+        public final Parcelable.Creator<TelemetryJSONFilePingStore> CREATOR = new Parcelable.Creator<TelemetryJSONFilePingStore>() {
+            @Override
+            public TelemetryJSONFilePingStore createFromParcel(Parcel source) {
+                return null;
+            }
+
+            @Override
+            public TelemetryJSONFilePingStore[] newArray(int size) {
+                return new TelemetryJSONFilePingStore[0];
+            }
+        };
+    }
+
+    private TelemetrySyncPingBundleBuilder builder;
+    private MockTelemetryPingStore syncPings = new MockTelemetryPingStore();
+    private MockTelemetryPingStore eventPings = new MockTelemetryPingStore();
+
+    @Before
+    public void setUp() throws Exception {
+        builder = new TelemetrySyncPingBundleBuilder();
+        builder.setReason(TelemetrySyncPingBundleBuilder.UPLOAD_REASON_SCHEDULE);
+        syncPings = new MockTelemetryPingStore();
+        eventPings = new MockTelemetryPingStore();
+    }
+
+    @Test
+    public void testGeneralShape() throws Exception {
+        builder.setSyncStore(syncPings);
+        builder.setSyncEventStore(eventPings);
+
+        TelemetryOutgoingPing outgoingPing = builder.build();
+
+        assertTrue(outgoingPing.getPayload().containsKey("application"));
+        assertTrue(outgoingPing.getPayload().containsKey("payload"));
+        assertTrue(outgoingPing.getPayload().containsKey("id"));
+        assertEquals("sync", outgoingPing.getPayload().getString("type"));
+        assertEquals(Integer.valueOf(4), outgoingPing.getPayload().getIntegerSafely("version"));
+
+        // Test application key.
+        ExtendedJSONObject application = outgoingPing.getPayload().getObject("application");
+        assertEquals("Mozilla", application.getString("vendor"));
+        assertTrue(application.containsKey("architecture"));
+        assertTrue(application.containsKey("platformVersion"));
+        assertTrue(application.containsKey("displayVersion"));
+        assertTrue(application.containsKey("version"));
+        assertTrue(application.containsKey("name"));
+        assertTrue(application.containsKey("channel"));
+        assertTrue(application.containsKey("buildID"));
+        assertTrue(application.containsKey("xpcomAbi"));
+
+        // Test general shape of payload.
+        // NB that even though we set an empty sync event store, it's not in the json string.
+        // That's because sync events are not yet instrumented.
+        ExtendedJSONObject payload = outgoingPing.getPayload().getObject("payload");
+        assertEquals("{\"syncs\":[],\"why\":\"schedule\"}", payload.toJSONString());
+    }
+
+    @Test
+    public void testBundlingOfMultiplePings() throws Exception {
+        // Try just one ping first.
+        syncPings.storePing(new TelemetrySyncPingBuilder()
+                .setDeviceID("test-device-id")
+                .setRestarted(true)
+                .setTook(123L)
+                .setUID("test-uid")
+                .build()
+        );
+        builder.setSyncStore(syncPings);
+
+        TelemetryOutgoingPing outgoingPing = builder.build();
+
+        // Ensure we have that one ping.
+        ExtendedJSONObject payload = outgoingPing.getPayload().getObject("payload");
+        assertEquals("schedule", payload.getString("why"));
+        JSONArray syncs = payload.getArray("syncs");
+        assertEquals(1, syncs.size());
+        assertSync((ExtendedJSONObject) syncs.get(0), "test-uid", 123L, "test-device-id", 1, true);
+
+        // Add another ping.
+        syncPings.storePing(new TelemetrySyncPingBuilder()
+                .setDeviceID("test-device-id")
+                .setRestarted(false)
+                .setTook(321L)
+                .setUID("test-uid")
+                .build()
+        );
+        builder.setSyncStore(syncPings);
+
+        // We should have two pings now.
+        outgoingPing = builder.build();
+        syncs = outgoingPing.getPayload()
+                .getObject("payload")
+                .getArray("syncs");
+        assertEquals(2, syncs.size());
+        assertSync((ExtendedJSONObject) syncs.get(0), "test-uid", 123L, "test-device-id", 1, true);
+        assertSync((ExtendedJSONObject) syncs.get(1), "test-uid", 321L, "test-device-id", 1, false);
+    }
+
+    private void assertSync(ExtendedJSONObject sync, String uid, long took, String deviceID, int version, boolean restarted) throws JSONException {
+        assertEquals(uid, sync.getString("uid"));
+        assertEquals(Long.valueOf(took), sync.getLong("took"));
+        assertEquals(deviceID, sync.getString("deviceID"));
+        assertEquals(Integer.valueOf(version), sync.getIntegerSafely("version"));
+        if (restarted) {
+            assertEquals(true, sync.getBoolean("restarted"));
+        } else {
+            assertFalse(sync.containsKey("restarted"));
+        }
+
+    }
+}
\ No newline at end of file
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java
@@ -34,17 +34,18 @@ public class TestTelemetryUploadAllPings
     @Before
     public void setUp() {
         testScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
         testStore = mock(TelemetryPingStore.class);
     }
 
     @Test
     public void testReadyToUpload() {
-        assertTrue("Scheduler is always ready to upload", testScheduler.isReadyToUpload(testStore));
+        assertTrue("Scheduler is always ready to upload", testScheduler.isReadyToUpload(
+                mock(Context.class), testStore));
     }
 
     @Test
     public void testScheduleUpload() {
         final Context context = mock(Context.class);
 
         testScheduler.scheduleUpload(context, testStore);
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
@@ -9,22 +9,22 @@ package org.mozilla.gecko.telemetry.stor
 import org.json.JSONObject;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
 import org.mozilla.gecko.telemetry.TelemetryPing;
 import org.mozilla.gecko.util.FileUtils;
 
 import java.io.File;
 import java.io.FileOutputStream;
-import java.io.FilenameFilter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
 
 import static org.junit.Assert.*;
@@ -98,40 +98,40 @@ public class TestTelemetryJSONFilePingSt
         new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw.
     }
 
     @Test
     public void testStorePingStoresCorrectData() throws Exception {
         assertStoreFileCount(0);
 
         final String expectedID = getDocID();
-        final TelemetryPing expectedPing = new TelemetryPing("a/server/url", generateTelemetryPayload(), expectedID);
+        final TelemetryOutgoingPing expectedPing = new TelemetryOutgoingPing("a/server/url", generateTelemetryPayload(), expectedID);
         testStore.storePing(expectedPing);
 
         assertStoreFileCount(1);
         final String filename = testDir.list()[0];
         assertTrue("Filename contains expected ID", filename.equals(expectedID));
         final JSONObject actual = FileUtils.readJSONObjectFromFile(new File(testDir, filename));
         assertEquals("Ping url paths are equal", expectedPing.getURLPath(), actual.getString(TelemetryJSONFilePingStore.KEY_URL_PATH));
         assertIsGeneratedPayload(new ExtendedJSONObject(actual.getString(TelemetryJSONFilePingStore.KEY_PAYLOAD)));
     }
 
     @Test
     public void testStorePingMultiplePingsStoreSeparateFiles() throws Exception {
         assertStoreFileCount(0);
         for (int i = 1; i < 10; ++i) {
-            testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID()));
+            testStore.storePing(new TelemetryOutgoingPing("server", generateTelemetryPayload(), getDocID()));
             assertStoreFileCount(i);
         }
     }
 
     @Test
     public void testStorePingReleasesFileLock() throws Exception {
         assertStoreFileCount(0);
-        testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID()));
+        testStore.storePing(new TelemetryOutgoingPing("server", generateTelemetryPayload(), getDocID()));
         assertStoreFileCount(1);
         final File file = new File(testDir, testDir.list()[0]);
         final FileOutputStream stream = new FileOutputStream(file);
         try {
             assertNotNull("File lock is released after store write", stream.getChannel().tryLock());
         } finally {
             stream.close(); // releases lock
         }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
@@ -49,16 +49,17 @@ import static org.junit.Assert.fail;
 public class TestTokenServerClient {
   public static final String JSON = "application/json";
   public static final String TEXT = "text/plain";
 
   public static final String TEST_TOKEN_RESPONSE = "{\"api_endpoint\": \"https://stage-aitc1.services.mozilla.com/1.0/1659259\"," +
       "\"duration\": 300," +
       "\"id\": \"eySHORTENED\"," +
       "\"key\": \"-plSHORTENED\"," +
+      "\"hashed_fxa_uid\": \"rAnD0MU1D\"," +
       "\"uid\": 1659259}";
 
   public static final String TEST_CONDITIONS_RESPONSE = "{\"errors\":[{" +
       "\"location\":\"header\"," +
       "\"description\":\"Need to accept conditions\"," +
       "\"condition_urls\":{\"tos\":\"http://url-to-tos.com\"}," +
       "\"name\":\"X-Conditions-Accepted\"}]," +
       "\"status\":\"error\"}";
--- a/mozglue/misc/StackWalk.cpp
+++ b/mozglue/misc/StackWalk.cpp
@@ -604,16 +604,19 @@ WalkStackMain64(struct WalkStackData* aD
 }
 
 static unsigned int WINAPI
 WalkStackThread(void* aData)
 {
   BOOL msgRet;
   MSG msg;
 
+  // XXX This is external linkage code.  Therefore, we cannot use WinUtils.
+  //     So, calls ::PeekMessage() and ::GetMessage() directly here.
+
   // Call PeekMessage to force creation of a message queue so that
   // other threads can safely post events to us.
   ::PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
 
   // and tell the thread that created us that we're ready.
   HANDLE readyEvent = (HANDLE)aData;
   ::SetEvent(readyEvent);
 
--- a/netwerk/protocol/http/Http2Session.cpp
+++ b/netwerk/protocol/http/Http2Session.cpp
@@ -959,16 +959,19 @@ Http2Session::SendHello()
     CreatePriorityNode(kBackgroundGroupID, 0, 0, "background");
     mNextStreamID += 2;
     MOZ_ASSERT(mNextStreamID == kSpeculativeGroupID);
     CreatePriorityNode(kSpeculativeGroupID, kBackgroundGroupID, 0, "speculative");
     mNextStreamID += 2;
     MOZ_ASSERT(mNextStreamID == kFollowerGroupID);
     CreatePriorityNode(kFollowerGroupID, kLeaderGroupID, 0, "follower");
     mNextStreamID += 2;
+    MOZ_ASSERT(mNextStreamID == kUrgentStartGroupID);
+    CreatePriorityNode(kUrgentStartGroupID, 0, 240, "urgentStart");
+    mNextStreamID += 2;
     // Hey, you! YES YOU! If you add/remove any groups here, you almost
     // certainly need to change the lookup of the stream/ID hash in
     // Http2Session::OnTransportStatus. Yeah, that's right. YOU!
   }
 
   FlushOutputQueue();
 }
 
@@ -2421,17 +2424,17 @@ Http2Session::OnTransportStatus(nsITrans
     // transaction on the session.
   case NS_NET_STATUS_RESOLVING_HOST:
   case NS_NET_STATUS_RESOLVED_HOST:
   case NS_NET_STATUS_CONNECTING_TO:
   case NS_NET_STATUS_CONNECTED_TO:
   case NS_NET_STATUS_TLS_HANDSHAKE_STARTING:
   case NS_NET_STATUS_TLS_HANDSHAKE_ENDED:
   {
-    Http2Stream *target = mStreamIDHash.Get(mUseH2Deps ? 0xD : 0x3);
+    Http2Stream *target = mStreamIDHash.Get(mUseH2Deps ? 0xF : 0x3);
     nsAHttpTransaction *transaction = target ? target->Transaction() : nullptr;
     if (transaction)
       transaction->OnTransportStatus(aTransport, aStatus, aProgress);
     break;
   }
 
   default:
     // The other transport events are ignored here because there is no good
--- a/netwerk/protocol/http/Http2Session.h
+++ b/netwerk/protocol/http/Http2Session.h
@@ -164,21 +164,22 @@ public:
   const static uint8_t kFrameLengthBytes = 3;
   const static uint8_t kFrameStreamIDBytes = 4;
   const static uint8_t kFrameFlagBytes = 1;
   const static uint8_t kFrameTypeBytes = 1;
   const static uint8_t kFrameHeaderBytes = kFrameLengthBytes + kFrameFlagBytes +
     kFrameTypeBytes + kFrameStreamIDBytes;
 
   enum {
-    kLeaderGroupID =     0x3,
+    kLeaderGroupID =      0x3,
     kOtherGroupID =       0x5,
     kBackgroundGroupID =  0x7,
     kSpeculativeGroupID = 0x9,
-    kFollowerGroupID =    0xB
+    kFollowerGroupID =    0xB,
+    kUrgentStartGroupID = 0xD
     // Hey, you! YES YOU! If you add/remove any groups here, you almost
     // certainly need to change the lookup of the stream/ID hash in
     // Http2Session::OnTransportStatus. Yeah, that's right. YOU!
   };
 
   static nsresult RecvHeaders(Http2Session *);
   static nsresult RecvPriority(Http2Session *);
   static nsresult RecvRstStream(Http2Session *);
--- a/netwerk/protocol/http/Http2Stream.cpp
+++ b/netwerk/protocol/http/Http2Stream.cpp
@@ -1208,42 +1208,46 @@ Http2Stream::UpdatePriorityDependency()
     return;
   }
 
   nsHttpTransaction *trans = mTransaction->QueryHttpTransaction();
   if (!trans) {
     return;
   }
 
-  // we create 5 fake dependency streams per session,
+  // we create 6 fake dependency streams per session,
   // these streams are never opened with HEADERS. our first opened stream is 0xd
   // 3 depends 0, weight 200, leader class (kLeaderGroupID)
   // 5 depends 0, weight 100, other (kOtherGroupID)
   // 7 depends 0, weight 0, background (kBackgroundGroupID)
   // 9 depends 7, weight 0, speculative (kSpeculativeGroupID)
   // b depends 3, weight 0, follower class (kFollowerGroupID)
+  // d depends 0, weight 240, urgent-start class (kUrgentStartGroupID)
   //
   // streams for leaders (html, js, css) depend on 3
   // streams for folowers (images) depend on b
   // default streams (xhr, async js) depend on 5
   // explicit bg streams (beacon, etc..) depend on 7
   // spculative bg streams depend on 9
+  // urgent-start streams depend on d
 
   uint32_t classFlags = trans->ClassOfService();
 
   if (classFlags & nsIClassOfService::Leader) {
     mPriorityDependency = Http2Session::kLeaderGroupID;
   } else if (classFlags & nsIClassOfService::Follower) {
     mPriorityDependency = Http2Session::kFollowerGroupID;
   } else if (classFlags & nsIClassOfService::Speculative) {
     mPriorityDependency = Http2Session::kSpeculativeGroupID;
   } else if (classFlags & nsIClassOfService::Background) {
     mPriorityDependency = Http2Session::kBackgroundGroupID;
   } else if (classFlags & nsIClassOfService::Unblocked) {
     mPriorityDependency = Http2Session::kOtherGroupID;
+  } else if (classFlags & nsIClassOfService::UrgentStart) {
+    mPriorityDependency = Http2Session::kUrgentStartGroupID;
   } else {
     mPriorityDependency = Http2Session::kFollowerGroupID; // unmarked followers
   }
 
   LOG3(("Http2Stream::UpdatePriorityDependency %p "
         "classFlags %X depends on stream 0x%X\n",
         this, classFlags, mPriorityDependency));
 }
--- a/python/mozbuild/mozbuild/frontend/gyp_reader.py
+++ b/python/mozbuild/mozbuild/frontend/gyp_reader.py
@@ -321,17 +321,25 @@ def process_gyp_result(gyp_result, gyp_d
               '/ipc/glue',
           ]
           # These get set via VC project file settings for normal GYP builds.
           if config.substs['OS_TARGET'] == 'WINNT':
               context['DEFINES']['UNICODE'] = True
               context['DEFINES']['_UNICODE'] = True
         context['DISABLE_STL_WRAPPING'] = True
 
-        context.update(gyp_dir_attrs.sandbox_vars)
+        for key, value in gyp_dir_attrs.sandbox_vars.items():
+            if context.get(key) and isinstance(context[key], list):
+                # If we have a key from sanbox_vars that's also been
+                # populated here we use the value from sandbox_vars as our
+                # basis rather than overriding outright.
+                context[key] = value + context[key]
+            else:
+                context[key] = value
+
         yield context
 
 
 # A version of gyp.Load that doesn't return the generator (because module objects
 # aren't Pickle-able, and we don't use it anyway).
 def load_gyp(*args):
     _, flat_list, targets, data = gyp.Load(*args)
     return flat_list, targets, data
--- a/security/manager/ssl/StaticHPKPins.h
+++ b/security/manager/ssl/StaticHPKPins.h
@@ -1156,9 +1156,9 @@ static const TransportSecurityPreload kP
   { "za.search.yahoo.com", false, true, false, -1, &kPinset_yahoo },
   { "zh.search.yahoo.com", false, true, false, -1, &kPinset_yahoo },
 };
 
 // Pinning Preload List Length = 480;
 
 static const int32_t kUnknownId = -1;
 
-static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1504884031179000);
+static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1504971037040000);
--- a/security/manager/ssl/nsSTSPreloadList.errors
+++ b/security/manager/ssl/nsSTSPreloadList.errors
@@ -23,17 +23,16 @@ 10tacle.io: could not connect to host
 123test.de: did not receive HSTS header
 126ium.moe: could not connect to host
 127011-networks.ch: could not connect to host
 12vpnchina.com: could not connect to host
 163pwd.com: could not connect to host
 16packets.com: could not connect to host
 188betwarriors.co.uk: could not connect to host
 188trafalgar.ca: did not receive HSTS header
-1981612088.rsc.cdn77.org: did not receive HSTS header
 1a-jva.de: could not connect to host
 1cover.com: could not connect to host
 1k8b.com: could not connect to host
 1password.com: did not receive HSTS header
 1s.tn: did not receive HSTS header
 1stcapital.com.sg: did not receive HSTS header
 1xcess.com: did not receive HSTS header
 1years.cc: could not connect to host
@@ -59,40 +58,37 @@ 360gradus.com: did not receive HSTS head
 365.or.jp: could not connect to host
 368mibn.com: could not connect to host
 38sihu.com: could not connect to host
 39sihu.com: could not connect to host
 3chit.cf: could not connect to host
 3click-loan.com: could not connect to host
 3delivered.com: could not connect to host
 3dproteinimaging.com: did not receive HSTS header
-3phase.pw: could not connect to host
 3sreporting.com: did not receive HSTS header
 3yearloans.com: max-age too low: 0
 404.sh: max-age too low: 0
 404404.info: could not connect to host
 420dongstorm.com: could not connect to host
 42ms.org: could not connect to host
-441jj.com: did not receive HSTS header
 4455software.com: did not receive HSTS header
 4679.space: could not connect to host
 4azino777.ru: did not receive HSTS header
 4cclothing.com: could not connect to host
 4elements.com: did not receive HSTS header
 4eyes.ch: did not receive HSTS header
 4miners.net: could not connect to host
 4sqsu.eu: could not connect to host
 4w-performers.link: could not connect to host
 50millionablaze.org: could not connect to host
 540.co: did not receive HSTS header
 56ct.com: could not connect to host
 60ych.net: did not receive HSTS header
 6120.eu: did not receive HSTS header
 69square.com: could not connect to host
-724go.com: could not connect to host
 7kovrikov.ru: did not receive HSTS header
 808.lv: could not connect to host
 83i.net: could not connect to host
 911911.pw: could not connect to host
 922.be: could not connect to host
 960news.ca: could not connect to host
 9651678.ru: could not connect to host
 99511.fi: did not receive HSTS header
@@ -132,37 +128,39 @@ accessacademies.org: max-age too low: 0
 accountradar.com: could not connect to host
 accounts-p.com: could not connect to host
 accuenergy.com: max-age too low: 0
 acelpb.com: did not receive HSTS header
 acevik.de: could not connect to host
 acg18.us: did not receive HSTS header
 acgmoon.org: did not receive HSTS header
 acisonline.net: did not receive HSTS header
+acmle.com: could not connect to host
 acorns.com: did not receive HSTS header
 acr.im: could not connect to host
 acslimited.co.uk: did not receive HSTS header
 activateplay.com: did not receive HSTS header
 activeweb.top: could not connect to host
 activiti.alfresco.com: did not receive HSTS header
 actu-medias.com: did not receive HSTS header
 acuve.jp: could not connect to host
 ada.is: max-age too low: 2592000
 adajwells.me: could not connect to host
+adamgold.net: could not connect to host
 adams.net: max-age too low: 0
 adamwk.com: did not receive HSTS header
 addaxpetroleum.com: could not connect to host
 addvocate.com: could not connect to host
 adelevie.com: could not connect to host
 adequatetechnology.com: could not connect to host
 aderal.io: could not connect to host
 adhs-chaoten.net: did not receive HSTS header
 adindexr.com: could not connect to host
 admin.google.com: did not receive HSTS header (error ignored - included regardless)
-admitcard.co.in: could not connect to host
+admitcard.co.in: did not receive HSTS header
 admsel.ec: could not connect to host
 adopteunsiteflash.com: did not receive HSTS header
 adquisitio.co.uk: could not connect to host
 adquisitio.de: could not connect to host
 adquisitio.es: could not connect to host
 adquisitio.fr: could not connect to host
 adquisitio.it: could not connect to host
 adrianseo.ro: did not receive HSTS header
@@ -193,16 +191,17 @@ agentseeker.ca: did not receive HSTS hea
 agevio.com: could not connect to host
 agrimap.com: did not receive HSTS header
 agrios.de: did not receive HSTS header
 agro-id.gov.ua: did not receive HSTS header
 ahabingo.com: did not receive HSTS header
 ahoynetwork.com: did not receive HSTS header
 ahri.ovh: could not connect to host
 aidanwoods.com: did not receive HSTS header
+aids.gov: did not receive HSTS header
 airbnb.com: did not receive HSTS header
 aircomms.com: did not receive HSTS header
 airlea.com: could not connect to host
 airproto.com: could not connect to host
 aishnair.com: could not connect to host
 aiticon.de: did not receive HSTS header
 aiw-thkoeln.online: could not connect to host
 ajmahal.com: could not connect to host
@@ -313,16 +312,17 @@ aniplus.cf: could not connect to host
 aniplus.gq: could not connect to host
 aniplus.ml: could not connect to host
 anitklib.ml: could not connect to host
 ankakaak.com: could not connect to host
 ankaraprofesyonelnakliyat.com: did not receive HSTS header
 ankaraprofesyonelnakliyat.com.tr: did not receive HSTS header
 annabellaw.com: did not receive HSTS header
 anomaly.ws: did not receive HSTS header
+anongoth.pl: could not connect to host
 anonymo.co.uk: could not connect to host
 anonymo.uk: could not connect to host
 anonymousstatecollegelulzsec.com: could not connect to host
 anook.com: max-age too low: 0
 another.ch: could not connect to host
 anstoncs.com.au: max-age too low: 86400
 ant.land: could not connect to host
 anthenor.co.uk: could not connect to host
@@ -404,25 +404,23 @@ asdpress.cn: could not connect to host
 ashlane-cottages.com: could not connect to host
 asianodor.com: could not connect to host
 ask.pe: could not connect to host
 askfit.cz: did not receive HSTS header
 asm-x.com: could not connect to host
 asmui.ga: could not connect to host
 asmui.ml: could not connect to host
 asr.cloud: could not connect to host
-asr.li: could not connect to host
 asr.rocks: could not connect to host
 asr.solar: could not connect to host
 asrob.eu: could not connect to host
 ass.org.au: did not receive HSTS header
 assekuranzjobs.de: could not connect to host
 asset-alive.com: did not receive HSTS header
 asset-alive.net: did not receive HSTS header
-asspinter.me: could not connect to host
 astrath.net: did not receive HSTS header
 astrolpost.com: could not connect to host
 astromelody.com: did not receive HSTS header
 asuhe.cc: did not receive HSTS header
 asuhe.win: did not receive HSTS header
 atavio.at: could not connect to host
 atavio.ch: could not connect to host
 atavio.de: did not receive HSTS header
@@ -473,17 +471,16 @@ aviacao.pt: did not receive HSTS header
 avinet.com: max-age too low: 0
 avqueen.cn: did not receive HSTS header
 avus-automobile.com: did not receive HSTS header
 awanderlustadventure.com: did not receive HSTS header
 awg-mode.de: did not receive HSTS header
 awxg.com: could not connect to host
 axado.com.br: did not receive HSTS header
 axeny.com: did not receive HSTS header
-ayothemes.com: could not connect to host
 ayuru.info: could not connect to host
 az.search.yahoo.com: did not receive HSTS header
 azino777.ru: did not receive HSTS header
 azort.com: did not receive HSTS header
 azprep.us: did not receive HSTS header
 b-landia.net: could not connect to host
 b303.me: did not receive HSTS header
 b3orion.com: max-age too low: 0
@@ -595,17 +592,16 @@ bestlashesandbrows.com: did not receive 
 betcafearena.ro: could not connect to host
 bethditto.com: did not receive HSTS header
 betnet.fr: could not connect to host
 betplanning.it: did not receive HSTS header
 bets.de: did not receive HSTS header
 bettween.com: could not connect to host
 betz.ro: did not receive HSTS header
 bevapehappy.com: did not receive HSTS header
-bevinco2020.com: did not receive HSTS header
 beyond-edge.com: could not connect to host
 beyuna.co.uk: did not receive HSTS header
 beyuna.eu: did not receive HSTS header
 beyuna.nl: did not receive HSTS header
 bezorg.ninja: could not connect to host
 bf.am: max-age too low: 0
 bfelob.gov: max-age too low: 86400
 bffm.biz: max-age too low: 0
@@ -616,16 +612,17 @@ bi.search.yahoo.com: did not receive HST
 biblerhymes.com: did not receive HSTS header
 bidon.ca: did not receive HSTS header
 bieberium.de: could not connect to host
 bienenblog.cc: could not connect to host
 big-black.de: did not receive HSTS header
 bigbbqbrush.bid: could not connect to host
 bigbrownpromotions.com.au: did not receive HSTS header
 bigshinylock.minazo.net: could not connect to host
+biguixhe.net: could not connect to host
 bildiri.ci: did not receive HSTS header
 bildschirmflackern.de: did not receive HSTS header
 billaud.eu.org: could not connect to host
 billin.net: did not receive HSTS header
 billkiss.com: could not connect to host
 billninja.com: did not receive HSTS header
 billrusling.com: could not connect to host
 binderapp.net: could not connect to host
@@ -676,16 +673,17 @@ blha303.com.au: could not connect to hos
 blindsexdate.nl: could not connect to host
 blitzprog.org: could not connect to host
 blocksatz-medien.de: did not receive HSTS header
 blog-ritaline.com: could not connect to host
 blog.cyveillance.com: did not receive HSTS header
 blog.torproject.org: did not receive HSTS header
 blogabout.ru: did not receive HSTS header
 bloglikepro.com: could not connect to host
+blowjs.com: could not connect to host
 blubbablasen.de: could not connect to host
 blucas.org: did not receive HSTS header
 blueglobalmedia.com: could not connect to host
 blueliv.com: did not receive HSTS header
 bluescloud.xyz: could not connect to host
 bluetenmeer.com: did not receive HSTS header
 bluketing.com: could not connect to host
 bluserv.net: did not receive HSTS header
@@ -737,16 +735,17 @@ brainvation.de: did not receive HSTS hea
 bran.cc: could not connect to host
 branchtrack.com: did not receive HSTS header
 brandnewdays.nl: could not connect to host
 brandon.so: could not connect to host
 brandred.net: could not connect to host
 brandspray.com: could not connect to host
 bravz.de: could not connect to host
 bregnedalsystems.dk: did not receive HSTS header
+bremensaki.com: could not connect to host
 brettabel.com: did not receive HSTS header
 brickoo.com: could not connect to host
 bridholm.se: could not connect to host
 brightstarkids.com.au: did not receive HSTS header
 britzer-toner.de: did not receive HSTS header
 brks.xyz: could not connect to host
 broken-oak.com: could not connect to host
 brokenhands.io: could not connect to host
@@ -804,17 +803,16 @@ by4cqb.cn: could not connect to host
 bydisk.com: could not connect to host
 bypassed.press: could not connect to host
 bypassed.today: did not receive HSTS header
 bypassed.works: did not receive HSTS header
 bypassed.world: did not receive HSTS header
 bypro.xyz: could not connect to host
 bysymphony.com: max-age too low: 0
 byte.wtf: did not receive HSTS header
-bytejail.com: could not connect to host
 bytema.re: could not connect to host
 bytepark.de: did not receive HSTS header
 bytesund.biz: could not connect to host
 c0rn3j.com: did not receive HSTS header
 c1yd3i.me: could not connect to host
 c3b.info: could not connect to host
 cabarave.com: could not connect to host
 cabsites.com: could not connect to host
@@ -824,17 +822,17 @@ cadao.me: did not receive HSTS header
 cadoth.net: did not receive HSTS header
 caesreon.com: could not connect to host
 cafe-murr.de: could not connect to host
 cafe-scientifique.org.ec: could not connect to host
 caim.cz: did not receive HSTS header
 cainhosting.com: did not receive HSTS header
 caizx.com: could not connect to host
 cajapopcorn.com: did not receive HSTS header
-cake.care: could not connect to host
+cake.care: did not receive HSTS header
 calcularpagerank.com.br: did not receive HSTS header
 calendarr.com: did not receive HSTS header
 calgaryconstructionjobs.com: did not receive HSTS header
 calix.com: max-age too low: 0
 callaction.co: max-age too low: 2628000
 calltrackingreports.com: could not connect to host
 caltonnutrition.com: did not receive HSTS header
 calvin.me: max-age too low: 2592000
@@ -883,20 +881,23 @@ cashmojo.com: could not connect to host
 cashmyphone.ch: could not connect to host
 casino-cashflow.ru: did not receive HSTS header
 casinostest.com: did not receive HSTS header
 casioshop.eu: did not receive HSTS header
 casovi.cf: could not connect to host
 catarsisvr.com: could not connect to host
 catinmay.com: did not receive HSTS header
 catnapstudios.com: could not connect to host
+cattivo.nl: did not receive HSTS header
+cavac.at: could not connect to host
 cavaleria.ro: did not receive HSTS header
 caveclan.org: did not receive HSTS header
 cavedevs.de: could not connect to host
 cavedroid.xyz: could not connect to host
+cbdev.de: could not connect to host
 cbengineeringinc.com: could not connect to host
 cbhq.net: could not connect to host
 ccblog.de: did not receive HSTS header
 cctech.ph: did not receive HSTS header
 cd.search.yahoo.com: did not receive HSTS header
 cd0.us: could not connect to host
 cdnb.co: could not connect to host
 cdndepo.com: could not connect to host
@@ -971,27 +972,27 @@ chotu.net: could not connect to host
 chris-web.info: could not connect to host
 chrisandsarahinasia.com: did not receive HSTS header
 chrisfaber.com: could not connect to host
 chriskyrouac.com: could not connect to host
 chrisopperwall.com: did not receive HSTS header
 christiaandruif.nl: could not connect to host
 christianbargon.de: did not receive HSTS header
 christianbro.gq: could not connect to host
-christianhoffmann.info: could not connect to host
 christophercolumbusfoundation.gov: did not receive HSTS header
 christophheich.me: could not connect to host
 chrisupjohn.com: could not connect to host
 chrome-devtools-frontend.appspot.com: did not receive HSTS header (error ignored - included regardless)
 chrome.google.com: did not receive HSTS header (error ignored - included regardless)
 chua.cf: could not connect to host
 chulado.com: did not receive HSTS header
 cidr.ml: could not connect to host
 cigarblogs.net: could not connect to host
 cigi.site: could not connect to host
+ciicutini.ro: did not receive HSTS header
 cim2b.de: could not connect to host
 cimalando.eu: could not connect to host
 cintdirect.com: could not connect to host
 ciplanutrition.com: did not receive HSTS header
 ciscommerce.net: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 citiagent.cz: could not connect to host
 cityoflaurel.org: did not receive HSTS header
 ciuciucadou.ro: could not connect to host
@@ -1066,17 +1067,16 @@ codealkemy.co: could not connect to host
 codeco.pw: could not connect to host
 codecontrollers.de: could not connect to host
 codeforce.io: could not connect to host
 codeforhakodate.org: could not connect to host
 codelayer.ca: could not connect to host
 codelitmus.com: did not receive HSTS header
 codemonkeyrawks.net: did not receive HSTS header
 codepoet.de: could not connect to host
-codepult.com: could not connect to host
 codepx.com: did not receive HSTS header
 codiva.io: max-age too low: 2592000
 coffeeetc.co.uk: did not receive HSTS header
 coffeestrategies.com: max-age too low: 0
 coiffeurschnittstelle.ch: did not receive HSTS header
 coindam.com: could not connect to host
 coldlostsick.net: could not connect to host
 collegepulse.org: did not receive HSTS header
@@ -1121,17 +1121,16 @@ contarkos.xyz: could not connect to host
 content-api-dev.azurewebsites.net: could not connect to host
 continuumgaming.com: could not connect to host
 controlcenter.gigahost.dk: did not receive HSTS header
 convert.zone: could not connect to host
 cooink.net: could not connect to host
 cookingreporter.com: did not receive HSTS header
 coolchevy.org.ua: did not receive HSTS header
 coole-meister.de: could not connect to host
-coolrc.me: could not connect to host
 cooxa.com: did not receive HSTS header
 cor-ser.es: could not connect to host
 coralproject.net: did not receive HSTS header
 coralrosado.com.br: did not receive HSTS header
 corbax.com: did not receive HSTS header
 corderoscleaning.com: did not receive HSTS header
 cordial-restaurant.com: did not receive HSTS header
 core.mx: could not connect to host
@@ -1141,16 +1140,18 @@ corgicloud.com: could not connect to hos
 corkyoga.site: could not connect to host
 cormactagging.ie: could not connect to host
 cormilu.com.br: did not receive HSTS header
 correctpaardbatterijnietje.nl: did not receive HSTS header
 corruption-mc.net: could not connect to host
 corruption-rsps.net: could not connect to host
 corruption-server.net: could not connect to host
 corsa-b.uk: could not connect to host
+coughlan.de: could not connect to host
+coumoul.fr: could not connect to host
 count.sh: could not connect to host
 countrybrewer.com.au: did not receive HSTS header
 courageousparentsnetwork.org: did not receive HSTS header
 couragewhispers.ca: did not receive HSTS header
 coursdeprogrammation.com: could not connect to host
 coursella.com: did not receive HSTS header
 covenantbank.net: could not connect to host
 coverduck.ru: could not connect to host
@@ -1227,16 +1228,18 @@ curlyroots.com: did not receive HSTS hea
 curroapp.com: could not connect to host
 curveweb.co.uk: did not receive HSTS header
 custe.rs: could not connect to host
 cuteys.de: max-age too low: 86400
 cutorrent.com: could not connect to host
 cuvva.eu: did not receive HSTS header
 cuvva.insure: did not receive HSTS header
 cuvva.io: did not receive HSTS header
+cuvva.it: did not receive HSTS header
+cuvva.me: did not receive HSTS header
 cuvva.org: did not receive HSTS header
 cyanogenmod.xxx: could not connect to host
 cyberdos.de: did not receive HSTS header
 cyberpunk.ca: could not connect to host
 cybershambles.com: could not connect to host
 cybersmart.co.uk: did not receive HSTS header
 cyberxpert.nl: could not connect to host
 cycleluxembourg.lu: did not receive HSTS header
@@ -1277,17 +1280,16 @@ dark-x.cf: could not connect to host
 darkengine.io: could not connect to host
 darkhole.cn: could not connect to host
 darkkeepers.dk: did not receive HSTS header
 darknebula.space: could not connect to host
 darkpony.ru: could not connect to host
 darksideof.it: could not connect to host
 darkstance.org: could not connect to host
 darrenellis.xyz: could not connect to host
-daryl.moe: could not connect to host
 dashburst.com: did not receive HSTS header
 dashnimorad.com: did not receive HSTS header
 data-abundance.com: could not connect to host
 datacalle.com: could not connect to host
 datahove.no: did not receive HSTS header
 datajapan.co.jp: could not connect to host
 datarank.com: max-age too low: 0
 dataretention.solutions: could not connect to host
@@ -1359,16 +1361,17 @@ desiccantpackets.com: did not receive HS
 designandmore.it: did not receive HSTS header
 designgears.com: did not receive HSTS header
 designthinking.or.jp: did not receive HSTS header
 desserteagleselvenar.tk: could not connect to host
 destinationbijoux.fr: could not connect to host
 destom.be: could not connect to host
 detector.exposed: could not connect to host
 detest.org: could not connect to host
+detutorial.com: max-age too low: 0
 deusu.org: did not receive HSTS header
 deux.solutions: could not connect to host
 deuxsol.co: could not connect to host
 deuxsol.com: could not connect to host
 deuxsolutions.com: could not connect to host
 deuxvia.com: could not connect to host
 devafterdark.com: could not connect to host
 devcu.com: could not connect to host
@@ -1399,17 +1402,16 @@ diezel.com: could not connect to host
 diferenca.com: did not receive HSTS header
 digioccumss.ddns.net: could not connect to host
 digitalbank.kz: could not connect to host
 digitaldaddy.net: could not connect to host
 digitalero.rip: did not receive HSTS header
 digitalriver.tk: could not connect to host
 digitalskillswap.com: could not connect to host
 dim.lighting: could not connect to host
-dimonb.com: could not connect to host
 dinamoelektrik.com: could not connect to host
 dineachook.com.au: did not receive HSTS header
 dingcc.me: could not connect to host
 dinkum.online: could not connect to host
 directhskincream.com: could not connect to host
 directorinegocis.cat: could not connect to host
 discovery.lookout.com: did not receive HSTS header
 dise-online.de: did not receive HSTS header
@@ -1465,16 +1467,17 @@ domaine-aigoual-cevennes.com: did not re
 domaris.de: did not receive HSTS header
 dominicpratt.de: did not receive HSTS header
 dominioanimal.com: could not connect to host
 dominique-mueller.de: did not receive HSTS header
 donmez.uk: could not connect to host
 donmez.ws: could not connect to host
 donttrustrobots.nl: could not connect to host
 donzelot.co.uk: max-age too low: 3600
+doodledraw.ninja: did not receive HSTS header
 dooku.cz: could not connect to host
 doomleika.com: could not connect to host
 doooonoooob.com: could not connect to host
 doridian.com: could not connect to host
 doridian.de: could not connect to host
 doridian.net: did not receive HSTS header
 doridian.org: could not connect to host
 dot42.no: could not connect to host
@@ -1537,17 +1540,16 @@ e-biografias.net: did not receive HSTS h
 e-deca2.org: did not receive HSTS header
 e-isfa.eu: did not receive HSTS header
 e-sa.com: did not receive HSTS header
 e3amn2l.com: could not connect to host
 earga.sm: could not connect to host
 earlybirdsnacks.com: could not connect to host
 earthrise16.com: could not connect to host
 easez.net: did not receive HSTS header
-eason-yang.com: could not connect to host
 easychiller.org: could not connect to host
 easyocm.hu: did not receive HSTS header
 easyplane.it: did not receive HSTS header
 eatlowcarb.de: did not receive HSTS header
 eauclairecommerce.com: could not connect to host
 ebankcbt.com: could not connect to host
 ebecs.com: did not receive HSTS header
 ebiografia.com: did not receive HSTS header
@@ -1628,33 +1630,36 @@ emilyhorsman.com: could not connect to h
 eminovic.me: could not connect to host
 emjainteractive.com: did not receive HSTS header
 emjimadhu.com: could not connect to host
 emmable.com: could not connect to host
 emnitech.com: could not connect to host
 empleosentorreon.mx: could not connect to host
 empleostampico.com: did not receive HSTS header
 empty-r.com: could not connect to host
+emrenovation.com: could not connect to host
 enaah.de: could not connect to host
 enargia.jp: max-age too low: 0
 encoder.pw: could not connect to host
 encontrebarato.com.br: did not receive HSTS header
 encrypted.google.com: did not receive HSTS header (error ignored - included regardless)
 encryptio.com: could not connect to host
 end.pp.ua: could not connect to host
 endlessdark.net: max-age too low: 600
+endlesshorizon.net: could not connect to host
 endlesstone.com: did not receive HSTS header
 enefan.jp: could not connect to host
 engelwerbung.com: did not receive HSTS header
 enginsight.com: did not receive HSTS header
 englishyamal.ru: did not receive HSTS header
 enigmacpt.com: did not receive HSTS header
 enigmail.net: did not receive HSTS header
 enjoy-nepal.de: max-age too low: 0
 enjoymayfield.com: max-age too low: 0
+enlightenment.org: did not receive HSTS header
 enskat.de: could not connect to host
 enskatson-sippe.de: could not connect to host
 enteente.club: could not connect to host
 enteente.space: could not connect to host
 enteente.xyz: could not connect to host
 enterdev.co: did not receive HSTS header
 enterprise-threat-monitor.com: max-age too low: 0
 enterprisecarclub.co.uk: did not receive HSTS header
@@ -1776,16 +1781,17 @@ fail4free.de: did not receive HSTS heade
 failforward.org: max-age too low: 0
 fairkey.dk: did not receive HSTS header
 fairlyoddtreasures.com: did not receive HSTS header
 faizan.net: could not connect to host
 faizan.xyz: could not connect to host
 fakeletters.org: did not receive HSTS header
 faktura.pl: did not receive HSTS header
 falconfrag.com: could not connect to host
+falkhusemann.de: could not connect to host
 falkp.no: did not receive HSTS header
 fallenangelspirits.uk: could not connect to host
 familie-sprink.de: could not connect to host
 familie-zimmermann.at: could not connect to host
 famio.cn: could not connect to host
 fantasyfootballpundit.com: max-age too low: 300
 fanyl.cn: could not connect to host
 farhadexchange.com: did not receive HSTS header
@@ -1855,17 +1861,16 @@ fish2.me: did not receive HSTS header
 fishlinemedia.com: max-age too low: 0
 fit4medien.de: did not receive HSTS header
 fitbylo.com: did not receive HSTS header
 fitiapp.com: could not connect to host
 fitnesswerk.de: could not connect to host
 five.vn: did not receive HSTS header
 fivestarsitters.com: did not receive HSTS header
 fivezerocreative.com: did not receive HSTS header
-fixate.ru: could not connect to host
 fixatom.com: did not receive HSTS header
 fixingdns.com: did not receive HSTS header
 fixtectools.co.za: could not connect to host
 fj.search.yahoo.com: did not receive HSTS header
 fjruiz.es: did not receive HSTS header
 fkcovering.be: could not connect to host
 flags.ninja: could not connect to host
 flamewall.net: could not connect to host
@@ -1910,16 +1915,17 @@ foraje-profesionale.ro: did not receive 
 forbook.net: could not connect to host
 fordbydesign.com: could not connect to host
 foreignexchangeresource.com: did not receive HSTS header
 foreveralone.io: could not connect to host
 forex-dan.com: did not receive HSTS header
 forgix.com: could not connect to host
 formazioneopen.it: could not connect to host
 formula.cf: could not connect to host
+forty8creates.com: did not receive HSTS header
 fotiu.com: could not connect to host
 fotm.net: max-age too low: 30000
 fotocerita.net: could not connect to host
 fotografosexpertos.com: did not receive HSTS header
 fotopasja.info: could not connect to host
 foudufafa.de: could not connect to host
 foxdev.io: did not receive HSTS header
 foxley-farm.co.uk: did not receive HSTS header
@@ -1934,16 +1940,17 @@ frankwei.xyz: did not receive HSTS heade
 franta.biz: did not receive HSTS header
 franta.email: did not receive HSTS header
 franzt.de: could not connect to host
 franzt.ovh: could not connect to host
 frasesdeamizade.pt: could not connect to host
 frasys.io: could not connect to host
 frasys.net: could not connect to host
 fraurichter.net: could not connect to host
+frederik-braun.com: did not receive HSTS header
 fredvoyage.fr: did not receive HSTS header
 freeflow.tv: could not connect to host
 freelanced.co.za: could not connect to host
 freematthale.net: did not receive HSTS header
 freenetproject.org: did not receive HSTS header
 freesoftwaredriver.com: did not receive HSTS header
 freethought.org.au: could not connect to host
 freeutopia.org: did not receive HSTS header
@@ -2027,24 +2034,27 @@ gameparade.de: could not connect to host
 gamepiece.com: could not connect to host
 gamers-life.fr: could not connect to host
 gamerslair.org: did not receive HSTS header
 gamerz-point.de: could not connect to host
 gamesdepartment.co.uk: did not receive HSTS header
 gameserver-sponsor.de: could not connect to host
 gamingmedia.eu: could not connect to host
 gampenhof.de: did not receive HSTS header
+gapdirect.com: could not connect to host
 gaptek.id: did not receive HSTS header
 garageon.net: did not receive HSTS header
 garciamartin.me: could not connect to host
 garden.trade: max-age too low: 0
 gatapro.net: could not connect to host
 gatorsa.es: did not receive HSTS header
 gdegem.org: did not receive HSTS header
 gdpventure.com: max-age too low: 0
+gdz-spishy.com: did not receive HSTS header
+gdz.tv: did not receive HSTS header
 gedankenbude.info: could not connect to host
 geekcast.co.uk: did not receive HSTS header
 geeky.software: could not connect to host
 geemo.top: could not connect to host
 geeq.ch: could not connect to host
 geli-graphics.com: did not receive HSTS header
 genuu.com: could not connect to host
 genuxation.com: could not connect to host
@@ -2119,17 +2129,16 @@ globalbridge-japan.com: did not receive 
 globalexpert.co.nz: could not connect to host
 globalittech.com: could not connect to host
 globalmusic.ga: could not connect to host
 globalsites.nl: did not receive HSTS header
 gm-assicurazioni.it: could not connect to host
 gm.search.yahoo.com: did not receive HSTS header
 gmail.com: did not receive HSTS header (error ignored - included regardless)
 gmoes.at: max-age too low: 600000
-gnetion.com: could not connect to host
 go.ax: did not receive HSTS header
 go2sh.de: did not receive HSTS header
 goabonga.com: could not connect to host
 goalsetup.com: did not receive HSTS header
 goaltree.ch: did not receive HSTS header
 goarmy.eu: could not connect to host
 goat.chat: did not receive HSTS header
 goat.xyz: did not receive HSTS header
@@ -2249,16 +2258,17 @@ gvt3.com: could not connect to host (err
 gw2reload.eu: could not connect to host
 gwijaya.com: could not connect to host
 gwtest.us: could not connect to host
 gxlrx.net: could not connect to host
 gyboche.com: could not connect to host
 gyboche.science: could not connect to host
 gycis.me: could not connect to host
 gylauto.fr: could not connect to host
+gyoza.beer: could not connect to host
 gypthecat.com: max-age too low: 604800
 gyz.io: could not connect to host
 h-og.com: could not connect to host
 h2check.org: could not connect to host
 haarkliniek.com: did not receive HSTS header
 habanaavenue.com: max-age too low: 0
 habbo.life: could not connect to host
 habbotalk.nl: could not connect to host
@@ -2337,23 +2347,26 @@ hdsmigrationtool.com: could not connect 
 hduin.xyz: could not connect to host
 hdwallpapers.net: did not receive HSTS header
 healtious.com: did not receive HSTS header
 heart.ge: did not receive HSTS header
 heartlandrentals.com: did not receive HSTS header
 hearty.space: could not connect to host
 heartyme.net: could not connect to host
 heathmanners.com: could not connect to host
+heavenlysmokenc.com: could not connect to host
 hebaus.com: could not connect to host
+hedonistic.org: could not connect to host
 heidilein.info: did not receive HSTS header
 heijblok.com: could not connect to host
 heimnetze.org: could not connect to host
 helgakristoffer.com: could not connect to host
 helgakristoffer.wedding: could not connect to host
 helingqi.com: could not connect to host
+hellomouse.cf: could not connect to host
 helloworldhost.com: did not receive HSTS header
 helpadmin.net: could not connect to host
 helpium.de: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 helpmebuild.com: did not receive HSTS header
 hemdal.se: could not connect to host
 hencagon.com: could not connect to host
 hepteract.us: could not connect to host
 heritagedentistry.ca: did not receive HSTS header
@@ -2377,16 +2390,17 @@ hikariempire.com: could not connect to h
 hilinemerchandising.com: did not receive HSTS header
 hillcity.org.nz: did not receive HSTS header
 hilnu.tk: could not connect to host
 hintergedanken.com: could not connect to host
 hipercultura.com: did not receive HSTS header
 hiphopconvention.nl: could not connect to host
 hitoy.org: did not receive HSTS header
 hittipps.com: did not receive HSTS header
+hiv.gov: did not receive HSTS header
 hjw-kunstwerk.de: could not connect to host
 hlyue.com: did not receive HSTS header
 hmm.nyc: could not connect to host
 hn.search.yahoo.com: did not receive HSTS header
 hodne.io: could not connect to host
 hoerbuecher-und-hoerspiele.de: could not connect to host
 hofiprojekt.cz: did not receive HSTS header
 hogar123.es: could not connect to host
@@ -2405,16 +2419,17 @@ horosho.in: could not connect to host
 horseboners.xxx: did not receive HSTS header
 hortifarm.ro: did not receive HSTS header
 horvathtom.com: could not connect to host
 hosteasy.nl: did not receive HSTS header
 hostedtalkgadget.google.com: did not receive HSTS header (error ignored - included regardless)
 hostelite.com: did not receive HSTS header
 hostgarou.com: did not receive HSTS header
 hostinaus.com.au: did not receive HSTS header
+hostingfj.com: did not receive HSTS header
 hostisan.com: could not connect to host
 hotartup.com: could not connect to host
 hotchillibox.co.za: could not connect to host
 hotchillibox.com: max-age too low: 0
 hotchoc.io: did not receive HSTS header
 hotelvue.nl: could not connect to host
 houkago-step.com: did not receive HSTS header
 housemaadiah.org: did not receive HSTS header
@@ -2446,17 +2461,16 @@ huskybutt.dog: could not connect to host
 hvh.no: could not connect to host
 hxying.com: could not connect to host
 hycken.com: did not receive HSTS header
 hydra.ws: could not connect to host
 hydra.zone: could not connect to host
 hydronium.cf: could not connect to host
 hydronium.ga: could not connect to host
 hydronium.me: could not connect to host
-hydronium.ml: could not connect to host
 hydronium.tk: could not connect to host
 hypa.net.au: did not receive HSTS header
 hyper69.com: did not receive HSTS header
 hysg.me: could not connect to host
 i-jp.net: could not connect to host
 i-partners.sk: did not receive HSTS header
 i10z.com: could not connect to host
 iamjoshellis.com: could not connect to host
@@ -2484,25 +2498,26 @@ idacmedia.com: max-age too low: 5184000
 ideal-envelopes.co.uk: did not receive HSTS header
 idealmykonos.com: did not receive HSTS header
 ideasmeetingpoint.com: could not connect to host
 ideation-inc.co.jp: could not connect to host
 idecode.net: could not connect to host
 idedr.com: could not connect to host
 identitylabs.uk: could not connect to host
 idgsupply.com: could not connect to host
-idinby.dk: did not receive HSTS header
+idid.tk: could not connect to host
 idlekernel.com: could not connect to host
 idontexist.me: did not receive HSTS header
 ie.search.yahoo.com: did not receive HSTS header
 iec.pe: could not connect to host
 ierna.com: did not receive HSTS header
 ies-italia.it: did not receive HSTS header
 ies.id.lv: could not connect to host
 ifad.org: did not receive HSTS header
+ifasec.de: could not connect to host
 ifastuniversity.com: did not receive HSTS header
 ifleurs.com: could not connect to host
 ifx.ee: could not connect to host
 ignatisd.gr: did not receive HSTS header
 igule.net: could not connect to host
 ihrlotto.de: could not connect to host
 ihrnationalrat.ch: could not connect to host
 ihsbsd.me: could not connect to host
@@ -2639,16 +2654,17 @@ iraqidinar.org: did not receive HSTS hea
 irazimina.ru: did not receive HSTS header
 irccloud.com: did not receive HSTS header
 iready.ro: could not connect to host
 irelandesign.com: did not receive HSTS header
 irmtrudjurke.de: did not receive HSTS header
 irukandjilabs.com: could not connect to host
 is-a-furry.org: did not receive HSTS header
 isabellehogarth.co.uk: could not connect to host
+isbc-telecom.ru: could not connect to host
 ischool.co.jp: did not receive HSTS header
 isdf.me: could not connect to host
 iseek.biz: max-age too low: 0
 iseulde.com: did not receive HSTS header
 ishillaryclintoninprisonyet.com: could not connect to host
 ishome.org: could not connect to host
 isitamor.pm: could not connect to host
 iskaz.rs: did not receive HSTS header
@@ -2658,17 +2674,16 @@ israkurort.com: did not receive HSTS hea
 istanbultravelguide.info: could not connect to host
 istaspirtslietas.lv: did not receive HSTS header
 it-go.net: did not receive HSTS header
 itechgeek.com: max-age too low: 0
 ithakama.com: did not receive HSTS header
 ithakama.cz: did not receive HSTS header
 itos.asia: did not receive HSTS header
 itos.pl: did not receive HSTS header
-itpros.ru: did not receive HSTS header
 itriskltd.com: could not connect to host
 itsadog.co.uk: did not receive HSTS header
 itsagadget.com: did not receive HSTS header
 itsamurai.ru: max-age too low: 2592000
 itsecurityassurance.pw: could not connect to host
 itsg-faq.de: could not connect to host
 itshost.ru: could not connect to host
 ivi-fertility.com: max-age too low: 0
@@ -2766,31 +2781,33 @@ jiaidu.com: could not connect to host
 jiangzm.com: could not connect to host
 jichi.io: did not receive HSTS header
 jikken.de: could not connect to host
 jimas.eu: did not receive HSTS header
 jimgao.tk: did not receive HSTS header
 jimmycai.org: could not connect to host
 jingyuesi.com: could not connect to host
 jirav.io: could not connect to host
+jitsi.org: did not receive HSTS header
 jiyue.com: could not connect to host
 jiyuu-ni.com: could not connect to host
 jiyuu-ni.net: could not connect to host
 jkb.pics: could not connect to host
 jkbuster.com: could not connect to host
 joakimalgroy.com: could not connect to host
 jobmedic.com: did not receive HSTS header
 jobss.co.uk: did not receive HSTS header
 joedavison.me: could not connect to host
 johannes-sprink.de: could not connect to host
 johnbrownphotography.ch: did not receive HSTS header
 johners.me: could not connect to host
 johnhgaunt.com: did not receive HSTS header
 johnrom.com: did not receive HSTS header
 jointoweb.com: could not connect to host
+jollausers.de: could not connect to host
 jonas-keidel.de: did not receive HSTS header
 jonasgroth.se: did not receive HSTS header
 jonathan.ir: could not connect to host
 jondarby.com: did not receive HSTS header
 jongha.me: could not connect to host
 jonn.me: could not connect to host
 joostbovee.nl: did not receive HSTS header
 jordanhamilton.me: could not connect to host
@@ -2844,37 +2861,37 @@ kaisers.de: did not receive HSTS header
 kaiyuewu.com: could not connect to host
 kalami.nl: could not connect to host
 kalevlamps.co.uk: could not connect to host
 kamalame.co: did not receive HSTS header
 kamikano.com: could not connect to host
 kanar.nl: could not connect to host
 kanehusky.com: could not connect to host
 kaneo-gmbh.de: did not receive HSTS header
-kaniklani.co.za: could not connect to host
 kaohub.com: could not connect to host
 kaplatz.is: could not connect to host
 kapucini.si: max-age too low: 0
 kaputt.com: could not connect to host
 karaoketonight.com: could not connect to host
+karhukamera.com: could not connect to host
 karlis-kavacis.id.lv: did not receive HSTS header
 karpanhellas.com: did not receive HSTS header
 kasilag.me: did not receive HSTS header
 katiaetdavid.fr: could not connect to host
 katproxy.online: could not connect to host
 katproxy.site: could not connect to host
 katproxy.tech: could not connect to host
 katproxy.top: could not connect to host
 kaufkraftkiel.de: could not connect to host
 kausch.at: did not receive HSTS header
 kavinvin.me: could not connect to host
 kawaiiku.com: could not connect to host
 kawaiiku.de: could not connect to host
 kaylyn.ink: could not connect to host
-kcolford.com: did not receive HSTS header
+kcolford.com: could not connect to host
 kd-plus.pp.ua: could not connect to host
 kdata.it: did not receive HSTS header
 kdm-online.de: did not receive HSTS header
 keeley.gq: could not connect to host
 keeley.ml: could not connect to host
 keeleysam.me: could not connect to host
 keepassa.co: could not connect to host
 keepclean.me: could not connect to host
@@ -2901,21 +2918,19 @@ kinderly.co.uk: did not receive HSTS hea
 kinderwagen-test24.de: could not connect to host
 kindof.ninja: could not connect to host
 kingmanhall.org: could not connect to host
 kinkdr.com: could not connect to host
 kinnon.enterprises: could not connect to host
 kintrip.com: did not receive HSTS header
 kionetworks.com: did not receive HSTS header
 kipin.fr: did not receive HSTS header
-kiraboshi.xyz: could not connect to host
 kirara.eu: could not connect to host
 kirkforcongress.com: could not connect to host
 kirkforsenate.com: could not connect to host
-kirkovsky.com: could not connect to host
 kirkpatrickdavis.com: could not connect to host
 kisa.io: could not connect to host
 kisalt.im: did not receive HSTS header
 kiss-register.org: did not receive HSTS header
 kissart.net: could not connect to host
 kisstyle.ru: did not receive HSTS header
 kitakemon.com: could not connect to host
 kitbag.com.au: did not receive HSTS header
@@ -2923,18 +2938,16 @@ kitchen-profi.ru: max-age too low: 25920
 kitchenchaos.de: could not connect to host
 kitchenpunx.com: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 kitk.at: could not connect to host
 kitsostech.com: could not connect to host
 kitsta.com: could not connect to host
 kiwiirc.com: max-age too low: 5256000
 kizil.net: could not connect to host
 kjaermaxi.me: did not receive HSTS header
-kksg.com: could not connect to host
-klausbrinch.dk: could not connect to host
 klauwd.com: did not receive HSTS header
 klaxn.org: could not connect to host
 kleertjesvoordelig.nl: did not receive HSTS header
 kleinblogje.nl: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 kleppe.co: could not connect to host
 kletterkater.com: did not receive HSTS header
 klicktojob.de: could not connect to host
 kmartin.io: did not receive HSTS header
@@ -2966,28 +2979,27 @@ kori.ml: could not connect to host
 korni22.org: did not receive HSTS header
 korsanparti.org: could not connect to host
 kostuumstore.nl: could not connect to host
 kotois.com: could not connect to host
 kotonehoko.net: could not connect to host
 kotovstyle.ru: could not connect to host
 koukni.cz: could not connect to host
 kourpe.online: could not connect to host
+kpebetka.net: could not connect to host
 kprog.net: could not connect to host
 kr.search.yahoo.com: did not receive HSTS header
 kraftfleisch.de: did not receive HSTS header
 kralik.xyz: could not connect to host
-krasovsky.me: could not connect to host
 krayx.com: did not receive HSTS header
 kreavis.com: did not receive HSTS header
 kreb.io: could not connect to host
 kredite.sale: could not connect to host
 krestanskydarek.cz: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 kriegt.es: did not receive HSTS header
-kristikala.nl: could not connect to host
 kristofferkoch.com: could not connect to host
 krizevackapajdasija.hr: could not connect to host
 krizevci.info: did not receive HSTS header
 kroetenfuchs.de: could not connect to host
 kroodle.nl: did not receive HSTS header
 kroon.email: could not connect to host
 krunut.com: did not receive HSTS header
 krypteia.org: could not connect to host
@@ -3025,16 +3037,17 @@ la-grande-jaugue.fr: did not receive HST
 labaia.info: could not connect to host
 labina.com.tr: did not receive HSTS header
 laboiteapc.fr: did not receive HSTS header
 labordata.io: did not receive HSTS header
 laborie.io: could not connect to host
 labrador-retrievers.com.au: did not receive HSTS header
 labs.directory: could not connect to host
 labs.moscow: did not receive HSTS header
+lacasabelucci.com: could not connect to host
 lacaverne.nl: could not connect to host
 lachlankidson.net: did not receive HSTS header
 lacledeslan.ninja: could not connect to host
 lacocinadelila.com: did not receive HSTS header
 laf.in.net: could not connect to host
 lagalerievirtuelle.fr: did not receive HSTS header
 lagoza.name: could not connect to host
 lalaya.fr: could not connect to host
@@ -3057,36 +3070,34 @@ laredsemanario.com: could not connect to
 laserfuchs.de: did not receive HSTS header
 lashstuff.com: did not receive HSTS header
 latelierdekathy.com: max-age too low: 86400
 latinred.com: could not connect to host
 latus.xyz: could not connect to host
 launchkey.com: did not receive HSTS header
 laurasplacefamilysupport.org.au: did not receive HSTS header
 lausitzer-widerstand.de: did not receive HSTS header
+lavalite.de: could not connect to host
 lavine.ch: did not receive HSTS header
 lavito.cz: did not receive HSTS header
 lavval.com: could not connect to host
 lawly.org: could not connect to host
 laxatus.com: could not connect to host
 laxiongames.es: could not connect to host
-lazerus.net: did not receive HSTS header
 lbrt.xyz: could not connect to host
 ldarby.me.uk: could not connect to host
 leadership9.com: could not connect to host
 leardev.de: did not receive HSTS header
 learnfrenchfluently.com: did not receive HSTS header
 learningorder.com: could not connect to host
 ledgerscope.net: could not connect to host
 leen.io: did not receive HSTS header
 leermotorrijden.nl: max-age too low: 300
 lefebvristes.com: could not connect to host
 lefebvristes.fr: could not connect to host
-lega-dental.com: could not connect to host
-lega3.dk: could not connect to host
 legarage.org: did not receive HSTS header
 leinir.dk: did not receive HSTS header
 leitner.com.au: did not receive HSTS header
 leiyun.me: did not receive HSTS header
 lellyboi.ml: could not connect to host
 lelongbank.com: did not receive HSTS header
 lemp.io: did not receive HSTS header
 lenovogaming.com: did not receive HSTS header
@@ -3127,16 +3138,17 @@ lianye1.cc: could not connect to host
 lianye2.cc: could not connect to host
 lianye3.cc: could not connect to host
 lianye4.cc: could not connect to host
 lianye5.cc: could not connect to host
 lianye6.cc: could not connect to host
 lianyexiuchang.in: could not connect to host
 liaoshuma.com: could not connect to host
 libanco.com: could not connect to host
+libbitcoin.org: could not connect to host
 libertyrp.org: could not connect to host
 library-quest.com: could not connect to host
 library.linode.com: did not receive HSTS header
 libreboot.org: did not receive HSTS header
 librechan.net: could not connect to host
 libreduca.com: could not connect to host
 lidl-selection.at: could not connect to host
 lidlovajogurteka.si: could not connect to host
@@ -3200,17 +3212,16 @@ locomotive.ca: did not receive HSTS head
 loftboard.eu: could not connect to host
 logario.com.br: could not connect to host
 logcat.info: could not connect to host
 logicaladvertising.com: could not connect to host
 login.corp.google.com: max-age too low: 7776000 (error ignored - included regardless)
 login.persona.org: could not connect to host
 loginseite.com: could not connect to host
 lognot.net: could not connect to host
-loli.bz: could not connect to host
 lolicore.ch: could not connect to host
 lolidunno.com: could not connect to host
 london-transfers.com: did not receive HSTS header
 londoncalling.co: did not receive HSTS header
 londonlanguageexchange.com: could not connect to host
 lonerwolf.com: did not receive HSTS header
 longboarding-ulm.de: could not connect to host
 look-at-my.site: could not connect to host
@@ -3314,16 +3325,17 @@ mailhost.it: could not connect to host
 maintainerheaven.ch: could not connect to host
 majesnix.org: did not receive HSTS header
 makeitdynamic.com: could not connect to host
 makerstuff.net: did not receive HSTS header
 malena.com.ua: did not receive HSTS header
 malenyflorist.com.au: did not receive HSTS header
 malerversand.de: did not receive HSTS header
 malfait.nl: could not connect to host
+malinheadview.ie: did not receive HSTS header
 maljaars-media.nl: could not connect to host
 malkaso.com.ua: could not connect to host
 malmstroms-co.se: could not connect to host
 maltes.website: could not connect to host
 malwaretips.com: did not receive HSTS header
 malwre.io: could not connect to host
 mamaison.io: could not connect to host
 mamaxi.org: did not receive HSTS header
@@ -3365,17 +3377,17 @@ marleyresort.com: did not receive HSTS h
 marriottvetcareers.com: could not connect to host
 marshut.net: could not connect to host
 martiert.com: could not connect to host
 martijnvhoof.nl: could not connect to host
 martinec.co.uk: could not connect to host
 martineve.com: did not receive HSTS header
 martinp.no: could not connect to host
 marumagic.com: did not receive HSTS header
-mashnew.com: did not receive HSTS header
+mashnew.com: could not connect to host
 masjidtawheed.net: did not receive HSTS header
 masterapi.ninja: did not receive HSTS header
 masteringtheterminal.com: did not receive HSTS header
 mastimtibetano.com: could not connect to host
 masty.nl: did not receive HSTS header
 matatall.com: did not receive HSTS header
 matchneedle.com: could not connect to host
 maternalsafety.org: did not receive HSTS header
@@ -3404,16 +3416,17 @@ mazz-tech.com: could not connect to host
 mc-team.org: could not connect to host
 mc81.com: could not connect to host
 mca2017.org: did not receive HSTS header
 mcard.vn: did not receive HSTS header
 mcc.re: could not connect to host
 mcdonalds.ru: did not receive HSTS header
 mcga.media: could not connect to host
 mclab.su: could not connect to host
+mclist.it: could not connect to host
 mdfnet.se: did not receive HSTS header
 mdscomp.net: did not receive HSTS header
 meadowfen.farm: could not connect to host
 meamod.com: max-age too low: 0
 mecenat-cassous.com: did not receive HSTS header
 mechmk1.me: did not receive HSTS header
 medallia.io: could not connect to host
 mediacru.sh: did not receive HSTS header
@@ -3454,20 +3467,22 @@ meredithkm.info: could not connect to ho
 meritz.rocks: could not connect to host
 mersinunivercity.com: did not receive HSTS header
 merson.me: could not connect to host
 meshlab.co: could not connect to host
 meshok.ru: did not receive HSTS header
 mesmoque.com: did not receive HSTS header
 metagrader.com: could not connect to host
 metebalci.com: did not receive HSTS header
+meteosherbrooke.com: could not connect to host
 meteosky.net: could not connect to host
 metin2blog.de: did not receive HSTS header
 metis.pw: could not connect to host
 meuemail.pro: could not connect to host
+mevo.xyz: did not receive HSTS header
 mexbt.com: could not connect to host
 mexicanbusinessweb.mx: did not receive HSTS header
 mexicansbook.ru: did not receive HSTS header
 mfcatalin.com: could not connect to host
 mh-bloemen.co.jp: could not connect to host
 mhdsyarif.com: did not receive HSTS header
 mhealthdemocamp.com: could not connect to host
 mhertel.com: did not receive HSTS header
@@ -3504,18 +3519,18 @@ mijn-email.org: could not connect to hos
 mikaelemilsson.net: did not receive HSTS header
 mikeburns.com: could not connect to host
 mikeg.de: did not receive HSTS header
 mikek.work: could not connect to host
 mikeology.org: could not connect to host
 mikeybot.com: could not connect to host
 mikonmaa.fi: could not connect to host
 mikrom.cz: did not receive HSTS header
-miku.be: could not connect to host
-miku.hatsune.my: did not receive HSTS header
+miku.be: did not receive HSTS header
+miku.hatsune.my: max-age too low: 5184000
 milatrans.pl: did not receive HSTS header
 milcoresonline.com: could not connect to host
 military-portal.cz: did not receive HSTS header
 mimoderoupa.pt: could not connect to host
 mindcraft.ga: could not connect to host
 mindoktor.se: did not receive HSTS header
 minecraftserverz.com: could not connect to host
 minecraftvoter.com: could not connect to host
@@ -3558,16 +3573,17 @@ mobilemedics.com: did not receive HSTS h
 mobilethreat.net: could not connect to host
 mobilethreatnetwork.net: could not connect to host
 mobilpass.no: could not connect to host
 mobiwalk.com: could not connect to host
 mobix5.com: did not receive HSTS header
 mocloud.eu: could not connect to host
 mocsuite.club: could not connect to host
 mocurio.com: could not connect to host
+modded-minecraft-server-list.com: could not connect to host
 moddedark.com: could not connect to host
 model9.io: did not receive HSTS header
 modemagazines.co.uk: could not connect to host
 modernibytovytextil.cz: could not connect to host
 modydev.club: could not connect to host
 moebel-nagel.de: did not receive HSTS header
 moellers.it: could not connect to host
 moelord.org: could not connect to host
@@ -3612,16 +3628,17 @@ morninglory.com: max-age too low: 259200
 mornings.com: did not receive HSTS header
 morpork.xyz: could not connect to host
 mortgagecentersmo.com: did not receive HSTS header
 mostwuat.com: could not connect to host
 motherbase.io: could not connect to host
 motionfreight.com: did not receive HSTS header
 motionpicturesolutions.com: did not receive HSTS header
 motocyklovedily.cz: did not receive HSTS header
+motorbiketourhanoi.com: could not connect to host
 motoryz.com: max-age too low: 300
 mottvd.com: could not connect to host
 moula.com.au: did not receive HSTS header
 mountainmusicpromotions.com: did not receive HSTS header
 moviesabout.net: could not connect to host
 moy-gorod.od.ua: did not receive HSTS header
 moy.cat: did not receive HSTS header
 mp3juices.is: could not connect to host
@@ -3659,16 +3676,17 @@ mushroomandfern.com: could not connect t
 musi.cx: could not connect to host
 musikkfondene.no: did not receive HSTS header
 mustika.cf: could not connect to host
 mutamatic.com: could not connect to host
 muzykaprzeszladoplay.pl: did not receive HSTS header
 mvanmarketing.nl: did not receive HSTS header
 mvsecurity.nl: could not connect to host
 mw.search.yahoo.com: did not receive HSTS header
+mwohlfarth.de: could not connect to host
 my-owncloud.com: could not connect to host
 my.alfresco.com: did not receive HSTS header
 my.swedbank.se: did not receive HSTS header
 myairshop.gr: could not connect to host
 myandroidtools.cc: could not connect to host
 mybudget.xyz: could not connect to host
 mybusiness.cm: did not receive HSTS header
 mychocolateweightloss.com: did not receive HSTS header
@@ -3694,16 +3712,17 @@ mypagella.it: could not connect to host
 mypension.ca: could not connect to host
 myphonebox.de: could not connect to host
 myrig.com: could not connect to host
 myrig.io: could not connect to host
 myrig.net: could not connect to host
 mysecretrewards.com: did not receive HSTS header
 mystery-science-theater-3000.de: did not receive HSTS header
 mythlogic.com: did not receive HSTS header
+mythslegendscollection.com: did not receive HSTS header
 myweb360.de: did not receive HSTS header
 myzone.com: did not receive HSTS header
 n0psled.nl: could not connect to host
 n2x.in: could not connect to host
 n4l.pw: could not connect to host
 nabru.co.uk: did not receive HSTS header
 nabytko.cz: did not receive HSTS header
 nadia.pt: could not connect to host
@@ -3771,31 +3790,30 @@ nestedquotes.ca: could not connect to ho
 net-navi.cc: max-age too low: 0
 netbox.cc: could not connect to host
 netherwind.eu: could not connect to host
 netlilo.com: could not connect to host
 netloanusa.com: could not connect to host
 netmagik.com: did not receive HSTS header
 netresourcedesign.com: did not receive HSTS header
 nettefoundation.com: could not connect to host
-networking4all.com: did not receive HSTS header
 networx-online.de: could not connect to host
 netzbit.de: could not connect to host
 netzpolitik.org: did not receive HSTS header
 netztest.at: did not receive HSTS header
 netzvieh.de: could not connect to host
 netzzwerg4u.de: could not connect to host
 neueonlinecasino2016.com: could not connect to host
 neuralgic.net: could not connect to host
 neuro-plus-100.com: could not connect to host
 neuronfactor.com: max-age too low: 1000
 never-afk.de: did not receive HSTS header
 neveta.com: could not connect to host
 newbieboss.com: did not receive HSTS header
-newcarrentalubon.com: did not receive HSTS header
+newcarrentalubon.com: max-age too low: 2629743
 newcitygas.ca: max-age too low: 0
 newedivideo.it: could not connect to host
 newgenerationplus.org: could not connect to host
 newhdmovies.io: did not receive HSTS header
 newkaliningrad.ru: did not receive HSTS header
 newlooknow.com: did not receive HSTS header
 newmelalife.com: did not receive HSTS header
 newportpropertygroup.com: could not connect to host
@@ -3823,20 +3841,23 @@ nicolasbettag.me: did not receive HSTS h
 niconiconi.xyz: could not connect to host
 niconode.com: could not connect to host
 niduxcomercial.com: could not connect to host
 nien.chat: could not connect to host
 nightwinds.tk: could not connect to host
 niho.jp: did not receive HSTS header
 nikcub.com: could not connect to host
 nikksno.io: did not receive HSTS header
+niklas.pw: could not connect to host
 niklaslindblad.se: did not receive HSTS header
+nikobradshaw.com: did not receive HSTS header
 nikomo.fi: did not receive HSTS header
 ninchisho-online.com: did not receive HSTS header
 ninhs.org: could not connect to host
+ninux.ch: could not connect to host
 nippler.org: did not receive HSTS header
 nippombashi.net: did not receive HSTS header
 nipponcareers.com: did not receive HSTS header
 nitrix.me: could not connect to host
 niva.synology.me: could not connect to host
 nixien.fr: could not connect to host
 nixmag.net: max-age too low: 2592000
 nll.fi: could not connect to host
@@ -3856,20 +3877,22 @@ nolberg.net: did not receive HSTS header
 nolimitsbook.de: did not receive HSTS header
 nolte.work: could not connect to host
 nomorebytes.de: could not connect to host
 nope.website: could not connect to host
 nopex.no: could not connect to host
 nopol.de: could not connect to host
 norandom.com: could not connect to host
 norb.at: could not connect to host
+nordor.homeip.net: could not connect to host
 nosecretshop.com: did not receive HSTS header
 notadd.com: did not receive HSTS header
 notenoughtime.de: could not connect to host
 nothing.net.nz: max-age too low: 7776000
+notify.moe: could not connect to host
 notinprod.com: did not receive HSTS header
 nottheonion.net: did not receive HSTS header
 nouvelle-vague-saint-cast.fr: did not receive HSTS header
 novaco.in: max-age too low: 3600
 novacoast.com: did not receive HSTS header
 novatrucking.de: could not connect to host
 nowak.ninja: did not receive HSTS header
 noworrywp.com: could not connect to host
@@ -3892,17 +3915,16 @@ nu3.fi: did not receive HSTS header
 nu3.fr: did not receive HSTS header
 nu3.no: did not receive HSTS header
 nu3.se: did not receive HSTS header
 nufla.de: could not connect to host
 null-sec.ru: could not connect to host
 null.cat: could not connect to host
 null.tips: could not connect to host
 nullpoint.at: did not receive HSTS header
-numberzero.org: could not connect to host
 numericacu.com: did not receive HSTS header
 numero-di-telefono.it: could not connect to host
 numista.com: did not receive HSTS header
 nurserybook.co: did not receive HSTS header
 nusatrip-api.com: did not receive HSTS header
 nutritionculture.com: could not connect to host
 nutsandboltsmedia.com: did not receive HSTS header
 nuttyveg.com: could not connect to host
@@ -4024,17 +4046,16 @@ optometriepunt.nl: did not receive HSTS 
 optumrxhealthstore.com: did not receive HSTS header
 oracaodocredo.com.br: could not connect to host
 orbiosales.com: could not connect to host
 orbitcom.de: max-age too low: 0
 orbograph-hrcm.com: could not connect to host
 ordereat.fr: could not connect to host
 orf-digitalsatkarte.at: could not connect to host
 organiplan.com: could not connect to host
-orhideous.name: could not connect to host
 originpc.com: did not receive HSTS header
 orioncustompcs.com: could not connect to host
 orionfcu.com: did not receive HSTS header
 orionrebellion.com: could not connect to host
 orleika.ml: could not connect to host
 orthodoxy.lt: did not receive HSTS header
 osaiyuwu.com: could not connect to host
 oscloud.com: could not connect to host
@@ -4157,30 +4178,30 @@ pcfun.net: could not connect to host
 pchax.net: could not connect to host
 pchospital.cc: did not receive HSTS header
 pdamsidoarjo.co.id: could not connect to host
 pdevio.com: could not connect to host
 pdf.yt: could not connect to host
 peakapp.nl: could not connect to host
 peerherrmann.de: could not connect to host
 peetah.com: max-age too low: 0
-peissen.com: could not connect to host
+peissen.com: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 pekkapikkarainen.fi: did not receive HSTS header
 pekkarik.ru: could not connect to host
 pengi.me: could not connect to host
 pengui.uk: could not connect to host
 penguinclientsystem.com: did not receive HSTS header
 pentano.net: could not connect to host
 peperiot.com: did not receive HSTS header
 pepperhead.com: did not receive HSTS header
 pepperworldhotshop.de: did not receive HSTS header
 perfect-radiant-wrinkles.com: could not connect to host
 perfectionis.me: could not connect to host
 performous.org: could not connect to host
-perfumista.vn: could not connect to host
+perfumista.vn: did not receive HSTS header
 perlwork.nl: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 perplex.nl: did not receive HSTS header
 perroud.pro: could not connect to host
 personaldatabasen.no: could not connect to host
 personalinjurylist.com: did not receive HSTS header
 personalizedtouch.co: could not connect to host
 perthdevicelab.com: did not receive HSTS header
 pesmyclub.com: did not receive HSTS header
@@ -4197,17 +4218,16 @@ pettsy.com: could not connect to host
 pewboards.com: could not connect to host
 pexieapp.com: did not receive HSTS header
 peytonfarrar.com: did not receive HSTS header
 pgpm.io: could not connect to host
 phalconist.com: did not receive HSTS header
 pharmgkb.org: could not connect to host
 phillipgoldfarb.com: could not connect to host
 phillprice.com: could not connect to host
-philpropertygroup.com: could not connect to host
 phoebe.co.nz: did not receive HSTS header
 phonenumberinfo.co.uk: could not connect to host
 phongmay24h.com: could not connect to host
 photoancestry.com: did not receive HSTS header
 photoblogverona.com: could not connect to host
 php-bach.org: could not connect to host
 phus.lu: did not receive HSTS header
 picardiascr.com: did not receive HSTS header
@@ -4278,16 +4298,17 @@ po.gl: did not receive HSTS header
 pocketsix.com: could not connect to host
 pocloud.homelinux.net: could not connect to host
 poed.net.au: could not connect to host
 poedgirl.com: could not connect to host
 poiema.com.sg: did not receive HSTS header
 pointeringles.com: could not connect to host
 pointiswunderland.de: did not receive HSTS header
 pointpro.de: did not receive HSTS header
+pokeduel.me: could not connect to host
 pol.in.th: could not connect to host
 pole.net.nz: could not connect to host
 policeiwitness.sg: could not connect to host
 polimat.org: could not connect to host
 politically-incorrect.xyz: could not connect to host
 politologos.org: did not receive HSTS header
 polycoise.com: could not connect to host
 polypho.nyc: could not connect to host
@@ -4297,17 +4318,16 @@ pontualcomp.com: could not connect to ho
 pony.today: could not connect to host
 poolsandstuff.com: did not receive HSTS header
 poon.tech: could not connect to host
 porno-gif.ru: did not receive HSTS header
 pornstars.me: did not receive HSTS header
 portalplatform.net: could not connect to host
 poshpak.com: max-age too low: 86400
 postcodewise.co.uk: did not receive HSTS header
-posterspy.com: did not receive HSTS header
 postscheduler.org: could not connect to host
 posylka.de: did not receive HSTS header
 potatoheads.net: could not connect to host
 potbar.com: could not connect to host
 potlytics.com: could not connect to host
 potsky.com: did not receive HSTS header
 poussinooz.fr: could not connect to host
 povitria.net: could not connect to host
@@ -4336,16 +4356,17 @@ prezola.com: did not receive HSTS header
 prgslab.net: could not connect to host
 pridoc.se: did not receive HSTS header
 printerest.io: could not connect to host
 printexpress.cloud: did not receive HSTS header
 printfn.com: could not connect to host
 priolkar.com: did not receive HSTS header
 privacylabs.io: did not receive HSTS header
 privacyrup.net: could not connect to host
+privytime.com: could not connect to host
 prnt.li: did not receive HSTS header
 pro-zone.com: could not connect to host
 prodpad.com: did not receive HSTS header
 production.vn: did not receive HSTS header
 profi-durchgangsmelder.de: did not receive HSTS header
 profivps.com: could not connect to host
 profundr.com: could not connect to host
 profusion.io: could not connect to host
@@ -4432,17 +4453,16 @@ quli.nl: did not receive HSTS header
 quotehex.com: could not connect to host
 quranserver.net: could not connect to host
 qvi.st: did not receive HSTS header
 qwaser.fr: could not connect to host
 qwilink.me: did not receive HSTS header
 r10n.com: did not receive HSTS header
 r15.me: could not connect to host
 r3bl.me: did not receive HSTS header
-r40.us: could not connect to host
 ra-schaal.de: could not connect to host
 raajheshkannaa.com: could not connect to host
 radicaleducation.net: could not connect to host
 radio-utopie.de: did not receive HSTS header
 radtke.bayern: could not connect to host
 rafaelcz.de: could not connect to host
 raidstone.com: could not connect to host
 raidstone.net: could not connect to host
@@ -4538,44 +4558,45 @@ remedioskaseros.com: did not receive HST
 remitatm.com: did not receive HSTS header
 remodela.com.ve: could not connect to host
 remonttitekniikka.fi: could not connect to host
 rene-schwarz.com: could not connect to host
 renideo.fr: could not connect to host
 renlong.org: did not receive HSTS header
 renrenss.com: did not receive HSTS header
 rentacarcluj.xyz: could not connect to host
-rentbrowser.com: could not connect to host
 rentbrowsertrain.me: could not connect to host
 rentcarassist.com: could not connect to host
 renteater.com: could not connect to host
 replacemychina.com: could not connect to host
 reprolife.co.uk: could not connect to host
 res-rheingau.de: did not receive HSTS header
 res42.com: could not connect to host
 resama.eu: did not receive HSTS header
 research.facebook.com: did not receive HSTS header
 reserve-online.net: did not receive HSTS header
 residentsinsurance.co.uk: did not receive HSTS header
+resl20.servehttp.com: could not connect to host
 respice.xyz: could not connect to host
 respostas.com.br: did not receive HSTS header
 restchart.com: did not receive HSTS header
 restrealitaet.de: did not receive HSTS header
 revapost.ch: could not connect to host
 revealdata.com: did not receive HSTS header
 revello.org: did not receive HSTS header
 revensoftware.com: could not connect to host
 reverie.pw: could not connect to host
 reviews.anime.my: max-age too low: 5184000
 revtut.net: did not receive HSTS header
 rewardstock.com: max-age too low: 0
 rex.st: could not connect to host
 rhapsodhy.hu: could not connect to host
 rhdigital.pro: could not connect to host
 rhering.de: could not connect to host
+richardjgreen.net: did not receive HSTS header
 richiemail.net: did not receive HSTS header
 richmondsunlight.com: did not receive HSTS header
 richmtdriver.com: could not connect to host
 richsiciliano.com: could not connect to host
 rid-wan.com: could not connect to host
 rideworks.com: did not receive HSTS header
 ridwan.co: did not receive HSTS header
 riesenmikrobe.de: max-age too low: 86400
@@ -4653,17 +4674,16 @@ rro.rs: could not connect to host
 rsajeey.info: could not connect to host
 rsampaio.info: could not connect to host
 rsf.io: could not connect to host
 rsmaps.org: could not connect to host
 rubbereggs.ca: could not connect to host
 rubberfurs.org: did not receive HSTS header
 rubecodeberg.com: could not connect to host
 rubenschulz.nl: did not receive HSTS header
-rubi-ka.net: could not connect to host
 ruborr.se: did not receive HSTS header
 rubysecurity.org: did not receive HSTS header
 rubyshop.nl: max-age too low: 604800
 rudeotter.com: did not receive HSTS header
 rugirlfriend.com: could not connect to host
 ruiming.me: did not receive HSTS header
 runawebinar.nl: could not connect to host
 runementors.com: could not connect to host
@@ -4757,16 +4777,17 @@ scooshonline.co.uk: did not receive HSTS
 scotbirchfield.com: did not receive HSTS header
 scottdial.com: did not receive HSTS header
 scottferguson.com.au: did not receive HSTS header
 scottgthomas.com: could not connect to host
 scottynordstrom.org: did not receive HSTS header
 scourt.info: did not receive HSTS header
 scourt.org.ua: did not receive HSTS header
 scrambl.is: could not connect to host
+scramble.io: could not connect to host
 scrambler.in: could not connect to host
 scrapings.net: could not connect to host
 screencaster.io: did not receive HSTS header
 screenresolution.space: could not connect to host
 screensaversplanet.com: did not receive HSTS header
 scribbleserver.com: could not connect to host
 scribe.systems: could not connect to host
 scrion.com: could not connect to host
@@ -4846,16 +4867,17 @@ seryo.moe: could not connect to host
 seryo.net: could not connect to host
 sethcaplan.com: could not connect to host
 setphaserstostun.org: could not connect to host
 setuid.de: could not connect to host
 setuid.io: did not receive HSTS header
 sevsey.ru: could not connect to host
 seyahatsagliksigortalari.com: could not connect to host
 sfsltd.com: did not receive HSTS header
+sftool.gov: could not connect to host
 shadoom.com: did not receive HSTS header
 shadowlurker.com.au: did not receive HSTS header
 shadowmorph.info: did not receive HSTS header
 shadowsocks.net: could not connect to host
 shanekoster.net: did not receive HSTS header
 shanesage.com: could not connect to host
 shang-yu.cn: did not receive HSTS header
 shapesedinburgh.co.uk: did not receive HSTS header
@@ -4863,17 +4885,17 @@ shareimg.xyz: could not connect to host
 sharepass.pw: could not connect to host
 sharevari.com: did not receive HSTS header
 shauncrowley.co.uk: could not connect to host
 shaunwheelhou.se: could not connect to host
 shellj.me: max-age too low: 86400
 shellsec.pw: did not receive HSTS header
 shereallyheals.com: could not connect to host
 shibe.club: could not connect to host
-shiftins.com: did not receive HSTS header
+shiftins.com: could not connect to host
 shiftplanning.com: did not receive HSTS header
 shiinko.com: could not connect to host
 shindorei.fr: did not receive HSTS header
 shinebijoux.com.br: could not connect to host
 shinju.moe: could not connect to host
 shiona.xyz: could not connect to host
 shirosaki.org: could not connect to host
 shm-forum.org.uk: could not connect to host
@@ -4894,34 +4916,35 @@ si.to: could not connect to host
 siammedia.co: could not connect to host
 siamsnus.com: did not receive HSTS header
 sichere-kartenakzeptanz.de: did not receive HSTS header
 siciliadigitale.pro: could not connect to host
 siddhant.me: max-age too low: 0
 siebens.net: could not connect to host
 sifls.com: could not connect to host
 sig6.org: could not connect to host
+sigsegv.run: could not connect to host
 sijimi.cn: did not receive HSTS header
 silaslova-ekb.ru: could not connect to host
 silentcircle.com: did not receive HSTS header
 silentcircle.org: could not connect to host
-silentexplosion.de: could not connect to host
+silentexplosion.de: did not receive HSTS header
 silentlink.io: could not connect to host
 silicagelpackets.ca: did not receive HSTS header
 silkebaekken.no: did not receive HSTS header
 silver-drachenkrieger.de: did not receive HSTS header
 silverhome.ninja: could not connect to host
 silverpvp.com: could not connect to host
 simbast.com: could not connect to host
 simbihaiti.com: did not receive HSTS header
 simfri.com: did not receive HSTS header
 simobilklub.si: did not receive HSTS header
 simod.org: could not connect to host
+simon-pokorny.com: did not receive HSTS header
 simon.butcher.name: max-age too low: 2629743
-simon.lc: could not connect to host
 simongong.net: did not receive HSTS header
 simpan.id: could not connect to host
 simpleai.net: max-age too low: 600
 simplelearner.com: could not connect to host
 simplepractice.com: did not receive HSTS header
 simplixos.org: could not connect to host
 simply-premium.com: did not receive HSTS header
 simus.fr: did not receive HSTS header
@@ -5000,17 +5023,16 @@ soccergif.com: could not connect to host
 soci.ml: did not receive HSTS header
 socialbillboard.com: could not connect to host
 socialhead.io: could not connect to host
 socialprize.com: could not connect to host
 socialspirit.com.br: did not receive HSTS header
 sockeye.cc: could not connect to host
 socomponents.co.uk: did not receive HSTS header
 sodacore.com: could not connect to host
-soe-server.com: could not connect to host
 software.rocks: could not connect to host
 sogeek.me: did not receive HSTS header
 sokolka.tv: did not receive HSTS header
 sol-3.de: did not receive HSTS header
 solidfuelappliancespares.co.uk: did not receive HSTS header
 solidus.systems: could not connect to host
 soll-i.ch: did not receive HSTS header
 solsystems.ru: could not connect to host
@@ -5042,16 +5064,17 @@ spacehq.org: could not connect to host
 spaggel.nl: could not connect to host
 sparelib.com: max-age too low: 3650
 spark.team: could not connect to host
 sparklingsparklers.com: did not receive HSTS header
 sparsa.army: could not connect to host
 spassstempel.de: max-age too low: 86400
 spauted.com: could not connect to host
 spdysync.com: could not connect to host
+specialedesigns.com: did not receive HSTS header
 speculor.net: could not connect to host
 speed-mailer.com: could not connect to host
 speedcounter.net: did not receive HSTS header
 speedy.lt: max-age too low: 0
 speedyprep.com: did not receive HSTS header
 speidel.com.tr: did not receive HSTS header
 spencerbaer.com: could not connect to host
 sperohub.io: could not connect to host
@@ -5086,27 +5109,27 @@ srmaximo.com: could not connect to host
 srna.sk: could not connect to host
 srrr.ca: could not connect to host
 ss.wtf: could not connect to host
 ssl.panoramio.com: did not receive HSTS header
 ssl.rip: could not connect to host
 ssmato.me: could not connect to host
 ssnc.org: max-age too low: 300
 sspanda.com: did not receive HSTS header
+ssrvpn.tech: did not receive HSTS header
 ssworld.ga: could not connect to host
 staack.com: could not connect to host
 stabletoken.com: could not connect to host
 stadjerspasonline.nl: could not connect to host
 stadtbauwerk.at: did not receive HSTS header
 staffjoy.com: did not receive HSTS header
 staffjoystaging.com: did not receive HSTS header
 stahl.xyz: could not connect to host
 stalkerhispano.com: max-age too low: 0
 stalschermer.nl: could not connect to host
-stamonicatourandtravel.com: could not connect to host
 standardssuck.org: did not receive HSTS header
 standingmist.com: did not receive HSTS header
 starandshield.com: did not receive HSTS header
 stargatepartners.com: did not receive HSTS header
 starmusic.ga: did not receive HSTS header
 starttraffic.com: did not receive HSTS header
 startuponcloud.com: max-age too low: 2678400
 startuppeople.co.uk: could not connect to host
@@ -5114,16 +5137,17 @@ stash.ai: did not receive HSTS header
 state-sponsored-actors.net: could not connect to host
 statementinsertsforless.com: did not receive HSTS header
 stateofexception.io: could not connect to host
 static.or.at: did not receive HSTS header
 staticanime.net: could not connect to host
 stationaryjourney.com: did not receive HSTS header
 stationcharlie.co.za: did not receive HSTS header
 stationnementdenuit.ca: did not receive HSTS header
+statistikian.com: did not receive HSTS header
 status-sprueche.de: did not receive HSTS header
 statuschecks.net: could not connect to host
 stayokhotelscdc-mailing.com: could not connect to host
 stcomex.com: did not receive HSTS header
 stdrc.cc: did not receive HSTS header
 steelbea.ms: could not connect to host
 stefanweiser.de: did not receive HSTS header
 stepbystep3d.com: did not receive HSTS header
@@ -5152,28 +5176,26 @@ storecove.com: did not receive HSTS head
 storeden.com: did not receive HSTS header
 storefrontify.com: did not receive HSTS header
 stormhub.org: could not connect to host
 stqry.com: did not receive HSTS header
 str0.at: did not receive HSTS header
 strasweb.fr: did not receive HSTS header
 strbt.de: could not connect to host
 strchr.com: did not receive HSTS header
-stream.pub: did not receive HSTS header
 streamingeverywhere.com: could not connect to host
 streamingmagazin.de: could not connect to host
 streampanel.net: did not receive HSTS header
 streams.dyndns.org: could not connect to host
 strictlysudo.com: could not connect to host
 strivephysmed.com: did not receive HSTS header
 stroeercrm.de: could not connect to host
 strongest-privacy.com: could not connect to host
 stuartbaxter.co: could not connect to host
 student-scientist.org: did not receive HSTS header
-student.andover.edu: could not connect to host
 studentresearcher.org: did not receive HSTS header
 studentskydenik.cz: could not connect to host
 studenttravel.cz: did not receive HSTS header
 studinf.xyz: could not connect to host
 studio-panic.com: did not receive HSTS header
 studiozelden.com: did not receive HSTS header
 studybay.com: did not receive HSTS header
 studydrive.net: did not receive HSTS header
@@ -5247,20 +5269,20 @@ syncclinicalstudy.com: could not connect
 syncer.jp: did not receive HSTS header
 syneic.com: did not receive HSTS header
 syno.gq: could not connect to host
 syntheticmotoroil.org: did not receive HSTS header
 syriatalk.biz: could not connect to host
 syriatalk.org: could not connect to host
 syrocon.ch: could not connect to host
 sys.tf: could not connect to host
-sysert.tv: could not connect to host
 sysgeek.cn: could not connect to host
 syso.name: could not connect to host
 syspen.space: did not receive HSTS header
+sysrq.tech: could not connect to host
 systemd.me: could not connect to host
 szaszm.tk: max-age too low: 0
 t-complex.space: could not connect to host
 t-ken.xyz: could not connect to host
 t-shirts4less.nl: did not receive HSTS header
 t-tz.com: could not connect to host
 t4x.org: did not receive HSTS header
 taabe.xyz: could not connect to host
@@ -5299,27 +5321,30 @@ tastyyy.co: could not connect to host
 tatilbus.com: could not connect to host
 tatt.io: could not connect to host
 tauchkater.de: could not connect to host
 tavopica.lt: did not receive HSTS header
 taxbench.com: could not connect to host
 taxiindenbosch.nl: did not receive HSTS header
 taxsnaps.co.nz: could not connect to host
 tazz.in: could not connect to host
+tbrss.com: did not receive HSTS header
 tc-bonito.de: did not receive HSTS header
 tcao.info: could not connect to host
 tcby45.xyz: could not connect to host
 tcdw.net: did not receive HSTS header
 tcl.ath.cx: did not receive HSTS header
 tcomms.org: max-age too low: 0
 tcp.expert: did not receive HSTS header
 tcwebvn.com: could not connect to host
 teachforcanada.ca: did not receive HSTS header
 team-teasers.com: could not connect to host
 teamblueridge.org: could not connect to host
+teampaddymurphy.ch: did not receive HSTS header
+teampaddymurphy.ie: did not receive HSTS header
 teamsocial.co: did not receive HSTS header
 teamzeus.cz: could not connect to host
 tech-finder.fr: could not connect to host
 tech55i.com: could not connect to host
 techandtux.de: could not connect to host
 techassist.io: did not receive HSTS header
 techelements.co: could not connect to host
 techhipster.net: could not connect to host
@@ -5371,16 +5396,17 @@ tf2stadium.com: did not receive HSTS hea
 tfcoms-sp-tracker-client.azurewebsites.net: could not connect to host
 tffans.com: could not connect to host
 tfl.lu: did not receive HSTS header
 tgr.re: could not connect to host
 thagki9.com: could not connect to host
 thai.land: did not receive HSTS header
 thaihostcool.com: did not receive HSTS header
 thailandpropertylistings.com: did not receive HSTS header
+thalan.fr: could not connect to host
 the-construct.com: could not connect to host
 the-delta.net.eu.org: could not connect to host
 the-sky-of-valkyries.com: could not connect to host
 theamateurs.net: did not receive HSTS header
 theater.cf: could not connect to host
 theberkshirescompany.com: could not connect to host
 thebigfail.net: could not connect to host
 thebrightons.co.uk: did not receive HSTS header
@@ -5422,49 +5448,49 @@ thepcweb.tk: could not connect to host
 thepiratebay.al: could not connect to host
 thepiratebay.poker: could not connect to host
 thepiratebay.tech: could not connect to host
 therevenge.me: could not connect to host
 therewill.be: could not connect to host
 theseed.io: could not connect to host
 thestack.xyz: could not connect to host
 thestagchorleywood.co.uk: did not receive HSTS header
+thetradinghall.com: could not connect to host
 theurbanyoga.com: did not receive HSTS header
 thevintagenews.com: max-age too low: 0
 thewebfellas.com: did not receive HSTS header
 theworkingeye.nl: could not connect to host
 thewp.pro: could not connect to host
 thezonders.com: did not receive HSTS header
 thierfreund.de: could not connect to host
 thinkcoding.de: could not connect to host
 thinkcoding.org: could not connect to host
 thinlyveiledcontempt.com: could not connect to host
 thirdpartytrade.com: did not receive HSTS header
 thirty5.net: did not receive HSTS header
 thisisacompletetest.ga: could not connect to host
 thisisforager.com: could not connect to host
 thiswebhost.com: did not receive HSTS header
-thomascloud.ddns.net: could not connect to host
 thomaskliszowski.fr: did not receive HSTS header
 thomasschweizer.net: could not connect to host
 thorncreek.net: did not receive HSTS header
 thriveapproach.co.uk: did not receive HSTS header
 thumbtack.com: did not receive HSTS header
 thusoy.com: did not receive HSTS header
 ti.blog.br: could not connect to host
-tibbitshall.ca: did not receive HSTS header
 tickettoaster.de: max-age too low: 0
 tickopa.co.uk: could not connect to host
 tickreport.com: did not receive HSTS header
 ticktock.today: did not receive HSTS header
 tictactux.de: could not connect to host
 tidmore.us: could not connect to host
 tie-online.org: did not receive HSTS header
 tiendschuurstraat.nl: could not connect to host
 tiensnet.com: did not receive HSTS header
+tierrarp.com: could not connect to host
 tightlineproductions.com: did not receive HSTS header
 tikutiku.pl: could not connect to host
 tildebot.com: could not connect to host
 tiliaze.be: could not connect to host
 tiliaze.eu: did not receive HSTS header
 tilkah.com.au: could not connect to host
 timbeilby.com: could not connect to host
 timbuktutimber.com: did not receive HSTS header
@@ -5515,17 +5541,17 @@ todo.is: did not receive HSTS header
 todobazar.es: could not connect to host
 togelonlinecommunity.com: did not receive HSTS header
 tokenloan.com: could not connect to host
 tokoone.com: did not receive HSTS header
 tokotamz.net: max-age too low: 0
 tollmanz.com: did not receive HSTS header
 tolud.com: could not connect to host
 tomeara.net: could not connect to host
-tomharris.tech: could not connect to host
+tomharris.tech: did not receive HSTS header
 tomlankhorst.nl: did not receive HSTS header
 tomli.me: could not connect to host
 tommsy.com: did not receive HSTS header
 tommyads.com: could not connect to host
 tonyfantjr.com: could not connect to host
 toomanypillows.com: could not connect to host
 top-stage.net: could not connect to host
 topdeskdev.net: could not connect to host
@@ -5539,17 +5565,16 @@ torlock.download: could not connect to h
 torrentdownloads.bid: could not connect to host
 torrentz.website: could not connect to host
 torsten-schmitz.net: could not connect to host
 tortugalife.de: could not connect to host
 torv.rocks: did not receive HSTS header
 tosecure.link: could not connect to host
 toshnix.com: could not connect to host
 toshub.com: could not connect to host
-totallylegitimatehosting.ru: could not connect to host
 totem-eshop.cz: could not connect to host
 toucedo.de: could not connect to host
 touchbasemail.com: did not receive HSTS header
 touchscreen-handy.de: did not receive HSTS header
 tourpeer.com: did not receive HSTS header
 toxme.se: did not receive HSTS header
 toyotamotala.se: could not connect to host
 tpbcdn.com: could not connect to host
@@ -5576,17 +5601,16 @@ treatprostatewithhifu.com: could not con
 treeby.net: could not connect to host
 trell.co.in: did not receive HSTS header
 trendberry.ru: could not connect to host
 trinityaffirmations.com: max-age too low: 0
 trinitycore.org: max-age too low: 2592000
 tripdelta.com: did not receive HSTS header
 tripinsider.club: did not receive HSTS header
 trixies-wish.nz: could not connect to host
-troedel-trolle.de: did not receive HSTS header
 troi.de: did not receive HSTS header
 trollme.me: could not connect to host
 true.ink: did not receive HSTS header
 truebred-labradors.com: could not connect to host
 trunkjunk.co: did not receive HSTS header
 trusteecar.com: did not receive HSTS header
 trustmeimfancy.com: could not connect to host
 trybind.com: could not connect to host
@@ -5627,18 +5651,16 @@ twinkseason.org: could not connect to ho
 twinkseason.xyz: could not connect to host
 twist.party: could not connect to host
 twogo.com: did not receive HSTS header
 twolinepassbrewing.com: could not connect to host
 tx041cap.org: did not receive HSTS header
 txclimbers.com: could not connect to host
 txf.pw: could not connect to host
 ty2u.com: did not receive HSTS header
-tyler.rs: could not connect to host
-tyleromeara.com: could not connect to host
 tylian.net: max-age too low: 0
 tyrelius.com: did not receive HSTS header
 tyroproducts.eu: did not receive HSTS header
 tzappa.net: could not connect to host
 u-blox.com: max-age too low: 0
 ua.search.yahoo.com: did not receive HSTS header
 uadp.pw: could not connect to host
 uber.com.au: did not receive HSTS header
@@ -5688,17 +5710,17 @@ unblockmyproxy.site: could not connect t
 unblockthe.site: could not connect to host
 unblockthe.top: could not connect to host
 unccdesign.club: could not connect to host
 unclegen.xyz: could not connect to host
 under30stravelinsurance.com.au: did not receive HSTS header
 unfiltered.nyc: did not receive HSTS header
 unhu.fr: could not connect to host
 uni-games.com: could not connect to host
-unicooo.com: could not connect to host
+unicooo.com: did not receive HSTS header
 unikitty-on-tour.com: could not connect to host
 unison.com: did not receive HSTS header
 unisyssecurity.com: did not receive HSTS header
 unitlabs.net: could not connect to host
 university4industry.com: did not receive HSTS header
 univz.com: could not connect to host
 unknownphenomena.net: could not connect to host
 unmanaged.space: did not receive HSTS header
@@ -5713,16 +5735,17 @@ uow.ninja: could not connect to host
 up1.ca: could not connect to host
 upaknship.com: did not receive HSTS header
 upboard.jp: could not connect to host
 upldr.pw: could not connect to host
 uprotect.it: could not connect to host
 upstats.eu: could not connect to host
 ur-lauber.de: did not receive HSTS header
 urandom.eu.org: did not receive HSTS header
+urbanstylestaging.com: did not receive HSTS header
 urbpic.com: could not connect to host
 urown.net: could not connect to host
 urphp.com: could not connect to host
 us-immigration.com: did not receive HSTS header
 usaab.org: did not receive HSTS header
 usbtypeccompliant.com: could not connect to host
 uscitizenship.info: did not receive HSTS header
 uscntalk.com: could not connect to host
@@ -5744,16 +5767,17 @@ uy.search.yahoo.com: did not receive HST
 uyym.com: could not connect to host
 uz.search.yahoo.com: did not receive HSTS header
 uzmandroid.net: could not connect to host
 uzmandroid.top: could not connect to host
 v2.pw: did not receive HSTS header
 v4veedu.com: could not connect to host
 vaddder.com: could not connect to host
 vaelma.fi: max-age too low: 600
+val-sec.com: could not connect to host
 valethound.com: could not connect to host
 valis.sx: could not connect to host
 valkyrja.xyz: did not receive HSTS header
 valleyridgepta.org: could not connect to host
 vallis.net: did not receive HSTS header
 valmagus.com: could not connect to host
 valopv.be: could not connect to host
 vampirism.eu: could not connect to host
@@ -5849,17 +5873,17 @@ vpip.net: could not connect to host
 vpl.me: did not receive HSTS header
 vpn-byen.dk: did not receive HSTS header
 vpnhot.com: did not receive HSTS header
 vps-szerver-berles.hu: could not connect to host
 vpsmojo.com: could not connect to host
 vratny.space: could not connect to host
 vrobert.fr: could not connect to host
 vsestiralnie.com: did not receive HSTS header
-vvl.me: did not receive HSTS header
+vvl.me: could not connect to host
 vxstream-sandbox.com: did not receive HSTS header
 vyncke.org: max-age too low: 2678400
 vzk.io: could not connect to host
 w.wiki: could not connect to host
 w4a.fr: did not receive HSTS header
 w4xzr.top: could not connect to host
 w4xzr.xyz: could not connect to host
 wachtwoordencheck.nl: could not connect to host
@@ -5914,17 +5938,16 @@ webm.to: could not connect to host
 webmail.mayfirst.org: did not receive HSTS header
 webmaniabr.com: did not receive HSTS header
 webmarketingfestival.it: did not receive HSTS header
 webninja.work: could not connect to host
 webnosql.com: could not connect to host
 webperformance.ru: max-age too low: 3600
 webpublica.pt: could not connect to host
 webrebels.org: could not connect to host
-websenat.de: [Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISiteSecurityService.processHeader]"  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: /builds/slave/m-cen-l64-periodicupdate-00000/getHSTSPreloadList.js :: processStsHeader :: line 118"  data: no]
 webstationservice.fr: could not connect to host
 webstellung.com: could not connect to host
 webstory.xyz: could not connect to host
 webswitch.io: could not connect to host
 webtechgadgetry.com: did not receive HSTS header
 webtiles.co.uk: could not connect to host
 webtobesocial.de: could not connect to host
 webuni.hu: did not receive HSTS header
@@ -5978,17 +6001,17 @@ wikiclash.info: could not connect to hos
 wikisports.eu: could not connect to host
 wilf1rst.com: could not connect to host
 willcipriano.com: could not connect to host
 william.si: did not receive HSTS header
 williamsapiens.com: could not connect to host
 willosagiede.com: did not receive HSTS header
 winaes.com: did not receive HSTS header
 winclient.cn: could not connect to host
-windowsphoneblog.it: did not receive HSTS header
+windowsphoneblog.it: max-age too low: 0
 winecodeavocado.com: could not connect to host
 winged.io: could not connect to host
 wingumd.net: could not connect to host
 winpack.cf: could not connect to host
 winpack.eu.org: could not connect to host
 winsec.nl: could not connect to host
 winshiplending.com: did not receive HSTS header
 wipply.com: did not receive HSTS header
@@ -6099,17 +6122,16 @@ ximens.me: could not connect to host
 xinbiji.cn: did not receive HSTS header
 xisa.it: could not connect to host
 xiyu.moe: could not connect to host
 xjoin.de: could not connect to host
 xmerak.com: could not connect to host
 xmonk.org: could not connect to host
 xmppwocky.net: could not connect to host
 xmr.my: could not connect to host
-xn--0kqx72g4gftob.com: could not connect to host
 xn--3lqt7ir4md4tzwa.cn: did not receive HSTS header
 xn--3lqt7ir4md4tzwa.xn--fiqs8s: did not receive HSTS header
 xn--3px.jp: could not connect to host
 xn--4dbjwf8c.cf: could not connect to host
 xn--4dbjwf8c.ga: could not connect to host
 xn--4dbjwf8c.gq: could not connect to host
 xn--4dbjwf8c.tk: could not connect to host
 xn--79q87uvkclvgd56ahq5a.net: did not receive HSTS header
@@ -6130,16 +6152,17 @@ xn--mgbbh2a9fub.xn--ngbc5azd: could not 
 xn--neb-tma3u8u.xyz: could not connect to host
 xn--seelenwchter-mcb.eu: could not connect to host
 xn--werner-schffer-fib.de: could not connect to host
 xn--xdtx3pfzbiw3ar8e7yedqrhui.com: could not connect to host
 xn--yoamomisuasbcn-ynb.com: could not connect to host
 xobox.me: could not connect to host
 xoffy.com: did not receive HSTS header
 xom.party: could not connect to host
+xombitmusic.com: did not receive HSTS header
 xor-a.net: could not connect to host
 xperiacodes.com: did not receive HSTS header
 xpi.fr: could not connect to host
 xsmobile.de: could not connect to host
 xtom.email: could not connect to host
 xtream-hosting.com: could not connect to host
 xtream-hosting.de: could not connect to host
 xtream-hosting.eu: could not connect to host
@@ -6187,17 +6210,16 @@ yoloprod.fr: could not connect to host
 yoloseo.com: could not connect to host
 yomepre.com: could not connect to host
 yoru.me: did not receive HSTS header
 youcontrol.ru: could not connect to host
 youngandunited.nl: did not receive HSTS header
 youon.tokyo: could not connect to host
 yourbapp.ch: could not connect to host
 yourcomputer.expert: did not receive HSTS header
-yourdaddy.dk: did not receive HSTS header
 yoursecondphone.co: could not connect to host
 yourstrongbox.com: could not connect to host
 yoyoost.duckdns.org: could not connect to host
 ypiresia.fr: could not connect to host
 ytcuber.xyz: could not connect to host
 yu.gg: did not receive HSTS header
 yu7.jp: did not receive HSTS header
 yuan.ga: did not receive HSTS header
@@ -6205,21 +6227,22 @@ yuhen.ru: did not receive HSTS header
 yuko.moe: could not connect to host
 yunzhu.li: did not receive HSTS header
 yunzhu.org: could not connect to host
 yutabon.com: could not connect to host
 yuushou.com: max-age too low: 0
 yux.io: did not receive HSTS header
 yuxingxin.com: could not connect to host
 ywei.org: could not connect to host
-yzal.io: did not receive HSTS header
+yzal.io: could not connect to host
 z3liff.com: could not connect to host
 z3liff.net: could not connect to host
 za.search.yahoo.com: did not receive HSTS header
 zadieheimlich.com: did not receive HSTS header
+zafirus.name: could not connect to host
 zahe.me: could not connect to host
 zahyantechnologies.com: could not connect to host
 zakoncontrol.com: could not connect to host
 zamorano.edu: could not connect to host
 zamos.ru: max-age too low: 0
 zanthra.com: could not connect to host
 zao.fi: could not connect to host
 zap.yt: could not connect to host
@@ -6278,11 +6301,10 @@ zqhong.com: could not connect to host
 ztan.tk: could not connect to host
 ztcaoll222.cn: did not receive HSTS header
 zubel.it: did not receive HSTS header
 zudomc.me: could not connect to host
 zulu7.com: could not connect to host
 zuviel.space: could not connect to host
 zvncloud.com: did not receive HSTS header
 zwollemagazine.nl: did not receive HSTS header
-zwy.me: could not connect to host
 zyf.pw: could not connect to host
 zymbit.com: did not receive HSTS header
--- a/security/manager/ssl/nsSTSPreloadList.inc
+++ b/security/manager/ssl/nsSTSPreloadList.inc
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*****************************************************************************/
 /* This is an automatically generated file. If you're not                    */
 /* nsSiteSecurityService.cpp, you shouldn't be #including it.     */
 /*****************************************************************************/
 
 #include <stdint.h>
-const PRTime gPreloadListExpirationTime = INT64_C(1507303218909000);
+const PRTime gPreloadListExpirationTime = INT64_C(1507390226607000);
 
 static const char kSTSHostTable[] = {
   /* "0.me.uk", true */ '0', '.', 'm', 'e', '.', 'u', 'k', '\0',
   /* "007-preisvergleich.de", true */ '0', '0', '7', '-', 'p', 'r', 'e', 'i', 's', 'v', 'e', 'r', 'g', 'l', 'e', 'i', 'c', 'h', '.', 'd', 'e', '\0',
   /* "00f.net", true */ '0', '0', 'f', '.', 'n', 'e', 't', '\0',
   /* "00wbf.com", true */ '0', '0', 'w', 'b', 'f', '.', 'c', 'o', 'm', '\0',
   /* "0100dev.com", true */ '0', '1', '0', '0', 'd', 'e', 'v', '.', 'c', 'o', 'm', '\0',
   /* "0100dev.nl", true */ '0', '1', '0', '0', 'd', 'e', 'v', '.', 'n', 'l', '\0',
@@ -112,16 +112,17 @@ static const char kSTSHostTable[] = {
   /* "173vpn.cn", true */ '1', '7', '3', 'v', 'p', 'n', '.', 'c', 'n', '\0',
   /* "174.net.nz", true */ '1', '7', '4', '.', 'n', 'e', 't', '.', 'n', 'z', '\0',
   /* "1750studios.com", false */ '1', '7', '5', '0', 's', 't', 'u', 'd', 'i', 'o', 's', '.', 'c', 'o', 'm', '\0',
   /* "17hats.com", true */ '1', '7', 'h', 'a', 't', 's', '.', 'c', 'o', 'm', '\0',
   /* "18f.gov", true */ '1', '8', 'f', '.', 'g', 'o', 'v', '\0',
   /* "18f.gsa.gov", false */ '1', '8', 'f', '.', 'g', 's', 'a', '.', 'g', 'o', 'v', '\0',
   /* "195gm.com", true */ '1', '9', '5', 'g', 'm', '.', 'c', 'o', 'm', '\0',
   /* "1972969867.rsc.cdn77.org", true */ '1', '9', '7', '2', '9', '6', '9', '8', '6', '7', '.', 'r', 's', 'c', '.', 'c', 'd', 'n', '7', '7', '.', 'o', 'r', 'g', '\0',
+  /* "1981612088.rsc.cdn77.org", true */ '1', '9', '8', '1', '6', '1', '2', '0', '8', '8', '.', 'r', 's', 'c', '.', 'c', 'd', 'n', '7', '7', '.', 'o', 'r', 'g', '\0',
   /* "1a-diamantscheiben.de", true */ '1', 'a', '-', 'd', 'i', 'a', 'm', 'a', 'n', 't', 's', 'c', 'h', 'e', 'i', 'b', 'e', 'n', '.', 'd', 'e', '\0',
   /* "1a-vermessung.at", true */ '1', 'a', '-', 'v', 'e', 'r', 'm', 'e', 's', 's', 'u', 'n', 'g', '.', 'a', 't', '\0',
   /* "1a-werkstattgeraete.de", true */ '1', 'a', '-', 'w', 'e', 'r', 'k', 's', 't', 'a', 't', 't', 'g', 'e', 'r', 'a', 'e', 't', 'e', '.', 'd', 'e', '\0',
   /* "1atic.com", true */ '1', 'a', 't', 'i', 'c', '.', 'c', 'o', 'm', '\0',
   /* "1co-jp.net", true */ '1', 'c', 'o', '-', 'j', 'p', '.', 'n', 'e', 't', '\0',
   /* "1cover.co.nz", true */ '1', 'c', 'o', 'v', 'e', 'r', '.', 'c', 'o', '.', 'n', 'z', '\0',
   /* "1cover.com.au", true */ '1', 'c', 'o', 'v', 'e', 'r', '.', 'c', 'o', 'm', '.', 'a', 'u', '\0',
   /* "1e9.nl", true */ '1', 'e', '9', '.', 'n', 'l', '\0',
@@ -234,16 +235,17 @@ static const char kSTSHostTable[] = {
   /* "3dcart.com", true */ '3', 'd', 'c', 'a', 'r', 't', '.', 'c', 'o', 'm', '\0',
   /* "3dmedium.de", true */ '3', 'd', 'm', 'e', 'd', 'i', 'u', 'm', '.', 'd', 'e', '\0',
   /* "3do3dont.com", true */ '3', 'd', 'o', '3', 'd', 'o', 'n', 't', '.', 'c', 'o', 'm', '\0',
   /* "3dprintsondemand.eu", true */ '3', 'd', 'p', 'r', 'i', 'n', 't', 's', 'o', 'n', 'd', 'e', 'm', 'a', 'n', 'd', '.', 'e', 'u', '\0',
   /* "3drenaline.com", true */ '3', 'd', 'r', 'e', 'n', 'a', 'l', 'i', 'n', 'e', '.', 'c', 'o', 'm', '\0',
   /* "3haeuserprojekt.org", true */ '3', 'h', 'a', 'e', 'u', 's', 'e', 'r', 'p', 'r', 'o', 'j', 'e', 'k', 't', '.', 'o', 'r', 'g', '\0',
   /* "3haueserprojekt.org", true */ '3', 'h', 'a', 'u', 'e', 's', 'e', 'r', 'p', 'r', 'o', 'j', 'e', 'k', 't', '.', 'o', 'r', 'g', '\0',
   /* "3hl0.net", true */ '3', 'h', 'l', '0', '.', 'n', 'e', 't', '\0',
+  /* "3phase.pw", true */ '3', 'p', 'h', 'a', 's', 'e', '.', 'p', 'w', '\0',
   /* "3r.org.uk", true */ '3', 'r', '.', 'o', 'r', 'g', '.', 'u', 'k', '\0',
   /* "3s-hosting.de", true */ '3', 's', '-', 'h', 'o', 's', 't', 'i', 'n', 'g', '.', 'd', 'e', '\0',
   /* "3timegear.com", true */ '3', 't', 'i', 'm', 'e', 'g', 'e', 'a', 'r', '.', 'c', 'o', 'm', '\0',
   /* "3trees.tk", true */ '3', 't', 'r', 'e', 'e', 's', '.', 't', 'k', '\0',
   /* "3v4l.org", true */ '3', 'v', '4', 'l', '.', 'o', 'r', 'g', '\0',
   /* "3weekdietworks.com", true */ '3', 'w', 'e', 'e', 'k', 'd', 'i', 'e', 't', 'w', 'o', 'r', 'k', 's', '.', 'c', 'o', 'm', '\0',
   /* "4-1-where.com", true */ '4', '-', '1', '-', 'w', 'h', 'e', 'r', 'e', '.', 'c', 'o', 'm', '\0',
   /* "4-it.de", true */ '4', '-', 'i', 't', '.', 'd', 'e', '\0',
@@ -252,16 +254,17 @@ static const char kSTSHostTable[] = {
   /* "404notfound.com.br", true */ '4', '0', '4', 'n', 'o', 't', 'f', 'o', 'u', 'n', 'd', '.', 'c', 'o', 'm', '.', 'b', 'r', '\0',
   /* "4096bit.de", true */ '4', '0', '9', '6', 'b', 'i', 't', '.', 'd', 'e', '\0',
   /* "41-where.com", true */ '4', '1', '-', 'w', 'h', 'e', 'r', 'e', '.', 'c', 'o', 'm', '\0',
   /* "41844.de", true */ '4', '1', '8', '4', '4', '.', 'd', 'e', '\0',
   /* "41where.com", true */ '4', '1', 'w', 'h', 'e', 'r', 'e', '.', 'c', 'o', 'm', '\0',
   /* "42day.info", true */ '4', '2', 'd', 'a', 'y', '.', 'i', 'n', 'f', 'o', '\0',
   /* "42entrepreneurs.fr", true */ '4', '2', 'e', 'n', 't', 'r', 'e', 'p', 'r', 'e', 'n', 'e', 'u', 'r', 's', '.', 'f', 'r', '\0',
   /* "439191.com", true */ '4', '3', '9', '1', '9', '1', '.', 'c', 'o', 'm', '\0',
+  /* "441jj.com", true */ '4', '4', '1', 'j', 'j', '.', 'c', 'o', 'm', '\0',
   /* "4500.co.il", true */ '4', '5', '0', '0', '.', 'c', 'o', '.', 'i', 'l', '\0',
   /* "491mhz.net", true */ '4', '9', '1', 'm', 'h', 'z', '.', 'n', 'e', 't', '\0',
   /* "49889.com", true */ '4', '9', '8', '8', '9', '.', 'c', 'o', 'm', '\0',
   /* "4d2.xyz", true */ '4', 'd', '2', '.', 'x', 'y', 'z', '\0',
   /* "4dbygg.se", true */ '4', 'd', 'b', 'y', 'g', 'g', '.', 's', 'e', '\0',
   /* "4decor.org", true */ '4', 'd', 'e', 'c', 'o', 'r', '.', 'o', 'r', 'g', '\0',
   /* "4freepress.com", true */ '4', 'f', 'r', 'e', 'e', 'p', 'r', 'e', 's', 's', '.', 'c', 'o', 'm', '\0',
   /* "4g-server.eu", false */ '4', 'g', '-', 's', 'e', 'r', 'v', 'e', 'r', '.', 'e', 'u', '\0',
@@ -296,16 +299,17 @@ static const char kSTSHostTable[] = {
   /* "6548877.com", true */ '6', '5', '4', '8', '8', '7', '7', '.', 'c', 'o', 'm', '\0',
   /* "660011.com", true */ '6', '6', '0', '0', '1', '1', '.', 'c', 'o', 'm', '\0',
   /* "6660111.ru", true */ '6', '6', '6', '0', '1', '1', '1', '.', 'r', 'u', '\0',
   /* "692b8c32.de", true */ '6', '9', '2', 'b', '8', 'c', '3', '2', '.', 'd', 'e', '\0',
   /* "6969.us", true */ '6', '9', '6', '9', '.', 'u', 's', '\0',
   /* "6t-montjoye.org", true */ '6', 't', '-', 'm', 'o', 'n', 't', 'j', 'o', 'y', 'e', '.', 'o', 'r', 'g', '\0',
   /* "700.az", true */ '7', '0', '0', '.', 'a', 'z', '\0',
   /* "7183.org", true */ '7', '1', '8', '3', '.', 'o', 'r', 'g', '\0',
+  /* "724go.com", true */ '7', '2', '4', 'g', 'o', '.', 'c', 'o', 'm', '\0',
   /* "7261696e626f77.net", true */ '7', '2', '6', '1', '6', '9', '6', 'e', '6', '2', '6', 'f', '7', '7', '.', 'n', 'e', 't', '\0',
   /* "746.com", true */ '7', '4', '6', '.', 'c', 'o', 'm', '\0',
   /* "762.ch", true */ '7', '6', '2', '.', 'c', 'h', '\0',
   /* "771122.com", true */ '7', '7', '1', '1', '2', '2', '.', 'c', 'o', 'm', '\0',
   /* "771122.tv", true */ '7', '7', '1', '1', '2', '2', '.', 't', 'v', '\0',
   /* "777coin.com", true */ '7', '7', '7', 'c', 'o', 'i', 'n', '.', 'c', 'o', 'm', '\0',
   /* "7careconnect.com", true */ '7', 'c', 'a', 'r', 'e', 'c', 'o', 'n', 'n', 'e', 'c', 't', '.', 'c', 'o', 'm', '\0',
   /* "7nw.eu", true */ '7', 'n', 'w', '.', 'e', 'u', '\0',
@@ -483,17 +487,16 @@ static const char kSTSHostTable[] = {
   /* "achterhoekseveiligheidsbeurs.nl", true */ 'a', 'c', 'h', 't', 'e', 'r', 'h', 'o', 'e', 'k', 's', 'e', 'v', 'e', 'i', 'l', 'i', 'g', 'h', 'e', 'i', 'd', 's', 'b', 'e', 'u', 'r', 's', '.', 'n', 'l', '\0',
   /* "achterstieg.dedyn.io", true */ 'a', 'c', 'h', 't', 'e', 'r', 's', 't', 'i', 'e', 'g', '.', 'd', 'e', 'd', 'y', 'n', '.', 'i', 'o', '\0',
   /* "achtzehn.eu", true */ 'a', 'c', 'h', 't', 'z', 'e', 'h', 'n', '.', 'e', 'u', '\0',
   /* "acidbin.co", true */ 'a', 'c', 'i', 'd', 'b', 'i', 'n', '.', 'c', 'o', '\0',
   /* "aciksite.com", true */ 'a', 'c', 'i', 'k', 's', 'i', 't', 'e', '.', 'c', 'o', 'm', '\0',
   /* "aclu.org", false */ 'a', 'c', 'l', 'u', '.', 'o', 'r', 'g', '\0',
   /* "acme.beer", true */ 'a', 'c', 'm', 'e', '.', 'b', 'e', 'e', 'r', '\0',
   /* "acmexyz123.info", true */ 'a', 'c', 'm', 'e', 'x', 'y', 'z', '1', '2', '3', '.', 'i', 'n', 'f', 'o', '\0',
-  /* "acmle.com", false */ 'a', 'c', 'm', 'l', 'e', '.', 'c', 'o', 'm', '\0',
   /* "acnpacific.com", true */ 'a', 'c', 'n', 'p', 'a', 'c', 'i', 'f', 'i', 'c', '.', 'c', 'o', 'm', '\0',
   /* "acoffeeshops.com", true */ 'a', 'c', 'o', 'f', 'f', 'e', 'e', 's', 'h', 'o', 'p', 's', '.', 'c', 'o', 'm', '\0',
   /* "acourse.io", true */ 'a', 'c', 'o', 'u', 'r', 's', 'e', '.', 'i', 'o', '\0',
   /* "acrepairdrippingsprings.com", true */ 'a', 'c', 'r', 'e', 'p', 'a', 'i', 'r', 'd', 'r', 'i', 'p', 'p', 'i', 'n', 'g', 's', 'p', 'r', 'i', 'n', 'g', 's', '.', 'c', 'o', 'm', '\0',
   /* "acritelli.com", false */ 'a', 'c', 'r', 'i', 't', 'e', 'l', 'l', 'i', '.', 'c', 'o', 'm', '\0',
   /* "acrossgw.com", true */ 'a', 'c', 'r', 'o', 's', 's', 'g', 'w', '.', 'c', 'o', 'm', '\0',
   /* "acrylicwifi.com", true */ 'a', 'c', 'r', 'y', 'l', 'i', 'c', 'w', 'i', 'f', 'i', '.', 'c', 'o', 'm', '\0',
   /* "acs-chantal.com", true */ 'a', 'c', 's', '-', 'c', 'h', 'a', 'n', 't', 'a', 'l', '.', 'c', 'o', 'm', '\0',
@@ -522,17 +525,16 @@ static const char kSTSHostTable[] = {
   /* "ad-notam.pt", true */ 'a', 'd', '-', 'n', 'o', 't', 'a', 'm', '.', 'p', 't', '\0',
   /* "ad-notam.us", true */ 'a', 'd', '-', 'n', 'o', 't', 'a', 'm', '.', 'u', 's', '\0',
   /* "ada.gov", true */ 'a', 'd', 'a', '.', 'g', 'o', 'v', '\0',
   /* "adalis.org", true */ 'a', 'd', 'a', 'l', 'i', 's', '.', 'o', 'r', 'g', '\0',
   /* "adam-kostecki.de", true */ 'a', 'd', 'a', 'm', '-', 'k', 'o', 's', 't', 'e', 'c', 'k', 'i', '.', 'd', 'e', '\0',
   /* "adambyers.com", true */ 'a', 'd', 'a', 'm', 'b', 'y', 'e', 'r', 's', '.', 'c', 'o', 'm', '\0',
   /* "adamdixon.co.uk", true */ 'a', 'd', 'a', 'm', 'd', 'i', 'x', 'o', 'n', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "adamek.online", true */ 'a', 'd', 'a', 'm', 'e', 'k', '.', 'o', 'n', 'l', 'i', 'n', 'e', '\0',
-  /* "adamgold.net", true */ 'a', 'd', 'a', 'm', 'g', 'o', 'l', 'd', '.', 'n', 'e', 't', '\0',
   /* "adamkaminski.com", true */ 'a', 'd', 'a', 'm', 'k', 'a', 'm', 'i', 'n', 's', 'k', 'i', '.', 'c', 'o', 'm', '\0',
   /* "adamkostecki.de", true */ 'a', 'd', 'a', 'm', 'k', 'o', 's', 't', 'e', 'c', 'k', 'i', '.', 'd', 'e', '\0',
   /* "adamoutler.com", true */ 'a', 'd', 'a', 'm', 'o', 'u', 't', 'l', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "adamradocz.com", true */ 'a', 'd', 'a', 'm', 'r', 'a', 'd', 'o', 'c', 'z', '.', 'c', 'o', 'm', '\0',
   /* "adamricheimer.com", true */ 'a', 'd', 'a', 'm', 'r', 'i', 'c', 'h', 'e', 'i', 'm', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "adams.dk", true */ 'a', 'd', 'a', 'm', 's', '.', 'd', 'k', '\0',
   /* "adamstas.com", true */ 'a', 'd', 'a', 'm', 's', 't', 'a', 's', '.', 'c', 'o', 'm', '\0',
   /* "adamwilcox.org", true */ 'a', 'd', 'a', 'm', 'w', 'i', 'l', 'c', 'o', 'x', '.', 'o', 'r', 'g', '\0',
@@ -717,17 +719,16 @@ static const char kSTSHostTable[] = {
   /* "aibenzi.com", true */ 'a', 'i', 'b', 'e', 'n', 'z', 'i', '.', 'c', 'o', 'm', '\0',
   /* "aicial.co.uk", true */ 'a', 'i', 'c', 'i', 'a', 'l', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "aicial.com", true */ 'a', 'i', 'c', 'i', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "aicial.com.au", true */ 'a', 'i', 'c', 'i', 'a', 'l', '.', 'c', 'o', 'm', '.', 'a', 'u', '\0',
   /* "aidanmontare.net", true */ 'a', 'i', 'd', 'a', 'n', 'm', 'o', 'n', 't', 'a', 'r', 'e', '.', 'n', 'e', 't', '\0',
   /* "aiden.link", true */ 'a', 'i', 'd', 'e', 'n', '.', 'l', 'i', 'n', 'k', '\0',
   /* "aidhan.net", true */ 'a', 'i', 'd', 'h', 'a', 'n', '.', 'n', 'e', 't', '\0',
   /* "aidikofflaw.com", true */ 'a', 'i', 'd', 'i', 'k', 'o', 'f', 'f', 'l', 'a', 'w', '.', 'c', 'o', 'm', '\0',
-  /* "aids.gov", true */ 'a', 'i', 'd', 's', '.', 'g', 'o', 'v', '\0',
   /* "aie.de", true */ 'a', 'i', 'e', '.', 'd', 'e', '\0',
   /* "aiesecarad.ro", true */ 'a', 'i', 'e', 's', 'e', 'c', 'a', 'r', 'a', 'd', '.', 'r', 'o', '\0',
   /* "aify.eu", true */ 'a', 'i', 'f', 'y', '.', 'e', 'u', '\0',
   /* "aigcev.org", true */ 'a', 'i', 'g', 'c', 'e', 'v', '.', 'o', 'r', 'g', '\0',
   /* "aikenorganics.com", true */ 'a', 'i', 'k', 'e', 'n', 'o', 'r', 'g', 'a', 'n', 'i', 'c', 's', '.', 'c', 'o', 'm', '\0',
   /* "aikido-club-limburg.de", true */ 'a', 'i', 'k', 'i', 'd', 'o', '-', 'c', 'l', 'u', 'b', '-', 'l', 'i', 'm', 'b', 'u', 'r', 'g', '.', 'd', 'e', '\0',
   /* "aikido-linz.at", true */ 'a', 'i', 'k', 'i', 'd', 'o', '-', 'l', 'i', 'n', 'z', '.', 'a', 't', '\0',
   /* "aikido-wels.at", true */ 'a', 'i', 'k', 'i', 'd', 'o', '-', 'w', 'e', 'l', 's', '.', 'a', 't', '\0',
@@ -1175,17 +1176,16 @@ static const char kSTSHostTable[] = {
   /* "annuaire-jcb.com", true */ 'a', 'n', 'n', 'u', 'a', 'i', 'r', 'e', '-', 'j', 'c', 'b', '.', 'c', 'o', 'm', '\0',
   /* "annuaire-photographe.fr", false */ 'a', 'n', 'n', 'u', 'a', 'i', 'r', 'e', '-', 'p', 'h', 'o', 't', 'o', 'g', 'r', 'a', 'p', 'h', 'e', '.', 'f', 'r', '\0',
   /* "anohana.org", true */ 'a', 'n', 'o', 'h', 'a', 'n', 'a', '.', 'o', 'r', 'g', '\0',
   /* "anojan.com", true */ 'a', 'n', 'o', 'j', 'a', 'n', '.', 'c', 'o', 'm', '\0',
   /* "anon-next.de", true */ 'a', 'n', 'o', 'n', '-', 'n', 'e', 'x', 't', '.', 'd', 'e', '\0',
   /* "anonboards.com", true */ 'a', 'n', 'o', 'n', 'b', 'o', 'a', 'r', 'd', 's', '.', 'c', 'o', 'm', '\0',
   /* "anoncom.net", true */ 'a', 'n', 'o', 'n', 'c', 'o', 'm', '.', 'n', 'e', 't', '\0',
   /* "anoneko.com", true */ 'a', 'n', 'o', 'n', 'e', 'k', 'o', '.', 'c', 'o', 'm', '\0',
-  /* "anongoth.pl", true */ 'a', 'n', 'o', 'n', 'g', 'o', 't', 'h', '.', 'p', 'l', '\0',
   /* "anonukradio.org", true */ 'a', 'n', 'o', 'n', 'u', 'k', 'r', 'a', 'd', 'i', 'o', '.', 'o', 'r', 'g', '\0',
   /* "anonym-surfen.de", true */ 'a', 'n', 'o', 'n', 'y', 'm', '-', 's', 'u', 'r', 'f', 'e', 'n', '.', 'd', 'e', '\0',
   /* "anonyme-spieler.at", true */ 'a', 'n', 'o', 'n', 'y', 'm', 'e', '-', 's', 'p', 'i', 'e', 'l', 'e', 'r', '.', 'a', 't', '\0',
   /* "anotherchef.com", true */ 'a', 'n', 'o', 't', 'h', 'e', 'r', 'c', 'h', 'e', 'f', '.', 'c', 'o', 'm', '\0',
   /* "anotherfatgeek.net", true */ 'a', 'n', 'o', 't', 'h', 'e', 'r', 'f', 'a', 't', 'g', 'e', 'e', 'k', '.', 'n', 'e', 't', '\0',
   /* "anothermanfilm.co.uk", true */ 'a', 'n', 'o', 't', 'h', 'e', 'r', 'm', 'a', 'n', 'f', 'i', 'l', 'm', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "anothermilan.net", true */ 'a', 'n', 'o', 't', 'h', 'e', 'r', 'm', 'i', 'l', 'a', 'n', '.', 'n', 'e', 't', '\0',
   /* "ans-delft.nl", true */ 'a', 'n', 's', '-', 'd', 'e', 'l', 'f', 't', '.', 'n', 'l', '\0',
@@ -1519,21 +1519,23 @@ static const char kSTSHostTable[] = {
   /* "askwhy.eu", true */ 'a', 's', 'k', 'w', 'h', 'y', '.', 'e', 'u', '\0',
   /* "aspargesgaarden.no", true */ 'a', 's', 'p', 'a', 'r', 'g', 'e', 's', 'g', 'a', 'a', 'r', 'd', 'e', 'n', '.', 'n', 'o', '\0',
   /* "asperti.com", true */ 'a', 's', 'p', 'e', 'r', 't', 'i', '.', 'c', 'o', 'm', '\0',
   /* "asphaltfruehling.de", true */ 'a', 's', 'p', 'h', 'a', 'l', 't', 'f', 'r', 'u', 'e', 'h', 'l', 'i', 'n', 'g', '.', 'd', 'e', '\0',
   /* "asphyxia.su", true */ 'a', 's', 'p', 'h', 'y', 'x', 'i', 'a', '.', 's', 'u', '\0',
   /* "aspiescentral.com", true */ 'a', 's', 'p', 'i', 'e', 's', 'c', 'e', 'n', 't', 'r', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "aspires.co.jp", true */ 'a', 's', 'p', 'i', 'r', 'e', 's', '.', 'c', 'o', '.', 'j', 'p', '\0',
   /* "aspisdata.com", true */ 'a', 's', 'p', 'i', 's', 'd', 'a', 't', 'a', '.', 'c', 'o', 'm', '\0',
+  /* "asr.li", true */ 'a', 's', 'r', '.', 'l', 'i', '\0',
   /* "assdecoeur.org", true */ 'a', 's', 's', 'd', 'e', 'c', 'o', 'e', 'u', 'r', '.', 'o', 'r', 'g', '\0',
   /* "asseenfromthesidecar.org", true */ 'a', 's', 's', 'e', 'e', 'n', 'f', 'r', 'o', 'm', 't', 'h', 'e', 's', 'i', 'd', 'e', 'c', 'a', 'r', '.', 'o', 'r', 'g', '\0',
   /* "assemble-together.org", true */ 'a', 's', 's', 'e', 'm', 'b', 'l', 'e', '-', 't', 'o', 'g', 'e', 't', 'h', 'e', 'r', '.', 'o', 'r', 'g', '\0',
   /* "assguidesporrentruy.ch", true */ 'a', 's', 's', 'g', 'u', 'i', 'd', 'e', 's', 'p', 'o', 'r', 'r', 'e', 'n', 't', 'r', 'u', 'y', '.', 'c', 'h', '\0',
   /* "assindia.nl", true */ 'a', 's', 's', 'i', 'n', 'd', 'i', 'a', '.', 'n', 'l', '\0',
+  /* "asspinter.me", true */ 'a', 's', 's', 'p', 'i', 'n', 't', 'e', 'r', '.', 'm', 'e', '\0',
   /* "assurancesmons.be", true */ 'a', 's', 's', 'u', 'r', 'a', 'n', 'c', 'e', 's', 'm', 'o', 'n', 's', '.', 'b', 'e', '\0',
   /* "asta-bar.de", false */ 'a', 's', 't', 'a', '-', 'b', 'a', 'r', '.', 'd', 'e', '\0',
   /* "astec-informatica.com", true */ 'a', 's', 't', 'e', 'c', '-', 'i', 'n', 'f', 'o', 'r', 'm', 'a', 't', 'i', 'c', 'a', '.', 'c', 'o', 'm', '\0',
   /* "astengox.com", true */ 'a', 's', 't', 'e', 'n', 'g', 'o', 'x', '.', 'c', 'o', 'm', '\0',
   /* "astenretail.com", true */ 'a', 's', 't', 'e', 'n', 'r', 'e', 't', 'a', 'i', 'l', '.', 'c', 'o', 'm', '\0',
   /* "astraalivankila.net", true */ 'a', 's', 't', 'r', 'a', 'a', 'l', 'i', 'v', 'a', 'n', 'k', 'i', 'l', 'a', '.', 'n', 'e', 't', '\0',
   /* "astrea-voetbal-groningen.nl", true */ 'a', 's', 't', 'r', 'e', 'a', '-', 'v', 'o', 'e', 't', 'b', 'a', 'l', '-', 'g', 'r', 'o', 'n', 'i', 'n', 'g', 'e', 'n', '.', 'n', 'l', '\0',
   /* "astronomie-fulda.de", true */ 'a', 's', 't', 'r', 'o', 'n', 'o', 'm', 'i', 'e', '-', 'f', 'u', 'l', 'd', 'a', '.', 'd', 'e', '\0',
@@ -1743,16 +1745,17 @@ static const char kSTSHostTable[] = {
   /* "axrec.de", true */ 'a', 'x', 'r', 'e', 'c', '.', 'd', 'e', '\0',
   /* "axtux.tk", true */ 'a', 'x', 't', 'u', 'x', '.', 't', 'k', '\0',
   /* "ayahuascaadvisor.com", true */ 'a', 'y', 'a', 'h', 'u', 'a', 's', 'c', 'a', 'a', 'd', 'v', 'i', 's', 'o', 'r', '.', 'c', 'o', 'm', '\0',
   /* "ayesh.me", true */ 'a', 'y', 'e', 's', 'h', '.', 'm', 'e', '\0',
   /* "aykutcevik.com", true */ 'a', 'y', 'k', 'u', 't', 'c', 'e', 'v', 'i', 'k', '.', 'c', 'o', 'm', '\0',
   /* "aylak.com", true */ 'a', 'y', 'l', 'a', 'k', '.', 'c', 'o', 'm', '\0',
   /* "aymerick.fr", true */ 'a', 'y', 'm', 'e', 'r', 'i', 'c', 'k', '.', 'f', 'r', '\0',
   /* "aymericlagier.com", true */ 'a', 'y', 'm', 'e', 'r', 'i', 'c', 'l', 'a', 'g', 'i', 'e', 'r', '.', 'c', 'o', 'm', '\0',
+  /* "ayothemes.com", true */ 'a', 'y', 'o', 't', 'h', 'e', 'm', 'e', 's', '.', 'c', 'o', 'm', '\0',
   /* "ayurveda-mantry.com", true */ 'a', 'y', 'u', 'r', 'v', 'e', 'd', 'a', '-', 'm', 'a', 'n', 't', 'r', 'y', '.', 'c', 'o', 'm', '\0',
   /* "ayurveda101.com", false */ 'a', 'y', 'u', 'r', 'v', 'e', 'd', 'a', '1', '0', '1', '.', 'c', 'o', 'm', '\0',
   /* "azabani.com", true */ 'a', 'z', 'a', 'b', 'a', 'n', 'i', '.', 'c', 'o', 'm', '\0',
   /* "azamra.com", true */ 'a', 'z', 'a', 'm', 'r', 'a', '.', 'c', 'o', 'm', '\0',
   /* "azazy.net", true */ 'a', 'z', 'a', 'z', 'y', '.', 'n', 'e', 't', '\0',
   /* "azbuki.by", true */ 'a', 'z', 'b', 'u', 'k', 'i', '.', 'b', 'y', '\0',
   /* "azia.info", true */ 'a', 'z', 'i', 'a', '.', 'i', 'n', 'f', 'o', '\0',
   /* "azimut.fr", true */ 'a', 'z', 'i', 'm', 'u', 't', '.', 'f', 'r', '\0',
@@ -2196,16 +2199,17 @@ static const char kSTSHostTable[] = {
   /* "bettrlifeapp.com", true */ 'b', 'e', 't', 't', 'r', 'l', 'i', 'f', 'e', 'a', 'p', 'p', '.', 'c', 'o', 'm', '\0',
   /* "betulashop.ch", true */ 'b', 'e', 't', 'u', 'l', 'a', 's', 'h', 'o', 'p', '.', 'c', 'h', '\0',
   /* "betwalker.com", true */ 'b', 'e', 't', 'w', 'a', 'l', 'k', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "beulahtabernacle.com", true */ 'b', 'e', 'u', 'l', 'a', 'h', 't', 'a', 'b', 'e', 'r', 'n', 'a', 'c', 'l', 'e', '.', 'c', 'o', 'm', '\0',
   /* "beulen.email", true */ 'b', 'e', 'u', 'l', 'e', 'n', '.', 'e', 'm', 'a', 'i', 'l', '\0',
   /* "beulen.pro", true */ 'b', 'e', 'u', 'l', 'e', 'n', '.', 'p', 'r', 'o', '\0',
   /* "bevedo.cz", true */ 'b', 'e', 'v', 'e', 'd', 'o', '.', 'c', 'z', '\0',
   /* "beveiligingscamerawestland.nl", true */ 'b', 'e', 'v', 'e', 'i', 'l', 'i', 'g', 'i', 'n', 'g', 's', 'c', 'a', 'm', 'e', 'r', 'a', 'w', 'e', 's', 't', 'l', 'a', 'n', 'd', '.', 'n', 'l', '\0',
+  /* "bevinco2020.com", true */ 'b', 'e', 'v', 'i', 'n', 'c', 'o', '2', '0', '2', '0', '.', 'c', 'o', 'm', '\0',
   /* "bevinsco.org", true */ 'b', 'e', 'v', 'i', 'n', 's', 'c', 'o', '.', 'o', 'r', 'g', '\0',
   /* "bewerbungsfibel.de", true */ 'b', 'e', 'w', 'e', 'r', 'b', 'u', 'n', 'g', 's', 'f', 'i', 'b', 'e', 'l', '.', 'd', 'e', '\0',
   /* "bexit-hosting.nl", true */ 'b', 'e', 'x', 'i', 't', '-', 'h', 'o', 's', 't', 'i', 'n', 'g', '.', 'n', 'l', '\0',
   /* "bexit-security.eu", true */ 'b', 'e', 'x', 'i', 't', '-', 's', 'e', 'c', 'u', 'r', 'i', 't', 'y', '.', 'e', 'u', '\0',
   /* "bexit-security.nl", true */ 'b', 'e', 'x', 'i', 't', '-', 's', 'e', 'c', 'u', 'r', 'i', 't', 'y', '.', 'n', 'l', '\0',
   /* "bexit.nl", true */ 'b', 'e', 'x', 'i', 't', '.', 'n', 'l', '\0',
   /* "bexithosting.nl", true */ 'b', 'e', 'x', 'i', 't', 'h', 'o', 's', 't', 'i', 'n', 'g', '.', 'n', 'l', '\0',
   /* "bey.io", true */ 'b', 'e', 'y', '.', 'i', 'o', '\0',
@@ -2269,17 +2273,16 @@ static const char kSTSHostTable[] = {
   /* "big-andy.co.uk", true */ 'b', 'i', 'g', '-', 'a', 'n', 'd', 'y', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "bigbluedoor.net", true */ 'b', 'i', 'g', 'b', 'l', 'u', 'e', 'd', 'o', 'o', 'r', '.', 'n', 'e', 't', '\0',
   /* "bigclassaction.com", true */ 'b', 'i', 'g', 'c', 'l', 'a', 's', 's', 'a', 'c', 't', 'i', 'o', 'n', '.', 'c', 'o', 'm', '\0',
   /* "bigdinosaur.org", true */ 'b', 'i', 'g', 'd', 'i', 'n', 'o', 's', 'a', 'u', 'r', '.', 'o', 'r', 'g', '\0',
   /* "biggreenexchange.com", true */ 'b', 'i', 'g', 'g', 'r', 'e', 'e', 'n', 'e', 'x', 'c', 'h', 'a', 'n', 'g', 'e', '.', 'c', 'o', 'm', '\0',
   /* "bight.ca", true */ 'b', 'i', 'g', 'h', 't', '.', 'c', 'a', '\0',
   /* "biglou.com", true */ 'b', 'i', 'g', 'l', 'o', 'u', '.', 'c', 'o', 'm', '\0',
   /* "bigskymontanalandforsale.com", true */ 'b', 'i', 'g', 's', 'k', 'y', 'm', 'o', 'n', 't', 'a', 'n', 'a', 'l', 'a', 'n', 'd', 'f', 'o', 'r', 's', 'a', 'l', 'e', '.', 'c', 'o', 'm', '\0',
-  /* "biguixhe.net", true */ 'b', 'i', 'g', 'u', 'i', 'x', 'h', 'e', '.', 'n', 'e', 't', '\0',
   /* "biilo.com", true */ 'b', 'i', 'i', 'l', 'o', '.', 'c', 'o', 'm', '\0',
   /* "bijuteriicualint.ro", true */ 'b', 'i', 'j', 'u', 't', 'e', 'r', 'i', 'i', 'c', 'u', 'a', 'l', 'i', 'n', 't', '.', 'r', 'o', '\0',
   /* "bike-kurse.ch", true */ 'b', 'i', 'k', 'e', '-', 'k', 'u', 'r', 's', 'e', '.', 'c', 'h', '\0',
   /* "bike-shack.com", true */ 'b', 'i', 'k', 'e', '-', 's', 'h', 'a', 'c', 'k', '.', 'c', 'o', 'm', '\0',
   /* "bikebay.it", true */ 'b', 'i', 'k', 'e', 'b', 'a', 'y', '.', 'i', 't', '\0',
   /* "biker.dating", true */ 'b', 'i', 'k', 'e', 'r', '.', 'd', 'a', 't', 'i', 'n', 'g', '\0',
   /* "bikermusic.net", true */ 'b', 'i', 'k', 'e', 'r', 'm', 'u', 's', 'i', 'c', '.', 'n', 'e', 't', '\0',
   /* "bikeshopitalia.com", true */ 'b', 'i', 'k', 'e', 's', 'h', 'o', 'p', 'i', 't', 'a', 'l', 'i', 'a', '.', 'c', 'o', 'm', '\0',
@@ -2538,17 +2541,16 @@ static const char kSTSHostTable[] = {
   /* "blogcuaviet.com", true */ 'b', 'l', 'o', 'g', 'c', 'u', 'a', 'v', 'i', 'e', 't', '.', 'c', 'o', 'm', '\0',
   /* "bloggingwithchildren.com", true */ 'b', 'l', 'o', 'g', 'g', 'i', 'n', 'g', 'w', 'i', 't', 'h', 'c', 'h', 'i', 'l', 'd', 'r', 'e', 'n', '.', 'c', 'o', 'm', '\0',
   /* "blognone.com", true */ 'b', 'l', 'o', 'g', 'n', 'o', 'n', 'e', '.', 'c', 'o', 'm', '\0',
   /* "blogreen.org", true */ 'b', 'l', 'o', 'g', 'r', 'e', 'e', 'n', '.', 'o', 'r', 'g', '\0',
   /* "blokuhaka.fr", true */ 'b', 'l', 'o', 'k', 'u', 'h', 'a', 'k', 'a', '.', 'f', 'r', '\0',
   /* "bloodsports.org", true */ 'b', 'l', 'o', 'o', 'd', 's', 'p', 'o', 'r', 't', 's', '.', 'o', 'r', 'g', '\0',
   /* "bloomnbud.com", true */ 'b', 'l', 'o', 'o', 'm', 'n', 'b', 'u', 'd', '.', 'c', 'o', 'm', '\0',
   /* "bloomzoomy.ru", true */ 'b', 'l', 'o', 'o', 'm', 'z', 'o', 'o', 'm', 'y', '.', 'r', 'u', '\0',
-  /* "blowjs.com", true */ 'b', 'l', 'o', 'w', 'j', 's', '.', 'c', 'o', 'm', '\0',
   /* "bltc.co", true */ 'b', 'l', 't', 'c', '.', 'c', 'o', '\0',
   /* "bltc.co.uk", true */ 'b', 'l', 't', 'c', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "bltc.com", true */ 'b', 'l', 't', 'c', '.', 'c', 'o', 'm', '\0',
   /* "bltc.net", true */ 'b', 'l', 't', 'c', '.', 'n', 'e', 't', '\0',
   /* "bltc.org", true */ 'b', 'l', 't', 'c', '.', 'o', 'r', 'g', '\0',
   /* "bltc.org.uk", true */ 'b', 'l', 't', 'c', '.', 'o', 'r', 'g', '.', 'u', 'k', '\0',
   /* "blubberladen.de", true */ 'b', 'l', 'u', 'b', 'b', 'e', 'r', 'l', 'a', 'd', 'e', 'n', '.', 'd', 'e', '\0',
   /* "blue-labs.org", true */ 'b', 'l', 'u', 'e', '-', 'l', 'a', 'b', 's', '.', 'o', 'r', 'g', '\0',
@@ -2782,17 +2784,16 @@ static const char kSTSHostTable[] = {
   /* "breeethretail.ru", true */ 'b', 'r', 'e', 'e', 'e', 't', 'h', 'r', 'e', 't', 'a', 'i', 'l', '.', 'r', 'u', '\0',
   /* "breest.net", true */ 'b', 'r', 'e', 'e', 's', 't', '.', 'n', 'e', 't', '\0',
   /* "breeswish.org", true */ 'b', 'r', 'e', 'e', 's', 'w', 'i', 's', 'h', '.', 'o', 'r', 'g', '\0',
   /* "breeyn.com", true */ 'b', 'r', 'e', 'e', 'y', 'n', '.', 'c', 'o', 'm', '\0',
   /* "brefy.com", true */ 'b', 'r', 'e', 'f', 'y', '.', 'c', 'o', 'm', '\0',
   /* "brege.org", true */ 'b', 'r', 'e', 'g', 'e', '.', 'o', 'r', 'g', '\0',
   /* "breitbild-beamer.de", true */ 'b', 'r', 'e', 'i', 't', 'b', 'i', 'l', 'd', '-', 'b', 'e', 'a', 'm', 'e', 'r', '.', 'd', 'e', '\0',
   /* "brejoc.com", true */ 'b', 'r', 'e', 'j', 'o', 'c', '.', 'c', 'o', 'm', '\0',
-  /* "bremensaki.com", true */ 'b', 'r', 'e', 'm', 'e', 'n', 's', 'a', 'k', 'i', '.', 'c', 'o', 'm', '\0',
   /* "brendanscherer.com", true */ 'b', 'r', 'e', 'n', 'd', 'a', 'n', 's', 'c', 'h', 'e', 'r', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "brentnewbury.com", true */ 'b', 'r', 'e', 'n', 't', 'n', 'e', 'w', 'b', 'u', 'r', 'y', '.', 'c', 'o', 'm', '\0',
   /* "brettcornwall.com", true */ 'b', 'r', 'e', 't', 't', 'c', 'o', 'r', 'n', 'w', 'a', 'l', 'l', '.', 'c', 'o', 'm', '\0',
   /* "bretz-hufer.de", true */ 'b', 'r', 'e', 't', 'z', '-', 'h', 'u', 'f', 'e', 'r', '.', 'd', 'e', '\0',
   /* "brevboxar.se", true */ 'b', 'r', 'e', 'v', 'b', 'o', 'x', 'a', 'r', '.', 's', 'e', '\0',
   /* "brewtrackr.com", true */ 'b', 'r', 'e', 'w', 't', 'r', 'a', 'c', 'k', 'r', '.', 'c', 'o', 'm', '\0',
   /* "brgins.com", true */ 'b', 'r', 'g', 'i', 'n', 's', '.', 'c', 'o', 'm', '\0',
   /* "brianalaway.com", true */ 'b', 'r', 'i', 'a', 'n', 'a', 'l', 'a', 'w', 'a', 'y', '.', 'c', 'o', 'm', '\0',
@@ -3038,16 +3039,17 @@ static const char kSTSHostTable[] = {
   /* "byrko.sk", true */ 'b', 'y', 'r', 'k', 'o', '.', 's', 'k', '\0',
   /* "byronr.com", true */ 'b', 'y', 'r', 'o', 'n', 'r', '.', 'c', 'o', 'm', '\0',
   /* "byronwade.com", true */ 'b', 'y', 'r', 'o', 'n', 'w', 'a', 'd', 'e', '.', 'c', 'o', 'm', '\0',
   /* "byrtz.de", true */ 'b', 'y', 'r', 't', 'z', '.', 'd', 'e', '\0',
   /* "byte-time.com", true */ 'b', 'y', 't', 'e', '-', 't', 'i', 'm', 'e', '.', 'c', 'o', 'm', '\0',
   /* "byte.chat", true */ 'b', 'y', 't', 'e', '.', 'c', 'h', 'a', 't', '\0',
   /* "bytearts.net", true */ 'b', 'y', 't', 'e', 'a', 'r', 't', 's', '.', 'n', 'e', 't', '\0',
   /* "bytebucket.org", true */ 'b', 'y', 't', 'e', 'b', 'u', 'c', 'k', 'e', 't', '.', 'o', 'r', 'g', '\0',
+  /* "bytejail.com", true */ 'b', 'y', 't', 'e', 'j', 'a', 'i', 'l', '.', 'c', 'o', 'm', '\0',
   /* "bytema.cz", true */ 'b', 'y', 't', 'e', 'm', 'a', '.', 'c', 'z', '\0',
   /* "byteowls.com", true */ 'b', 'y', 't', 'e', 'o', 'w', 'l', 's', '.', 'c', 'o', 'm', '\0',
   /* "bytes.fyi", true */ 'b', 'y', 't', 'e', 's', '.', 'f', 'y', 'i', '\0',
   /* "bytesatwork.de", true */ 'b', 'y', 't', 'e', 's', 'a', 't', 'w', 'o', 'r', 'k', '.', 'd', 'e', '\0',
   /* "bytesatwork.eu", true */ 'b', 'y', 't', 'e', 's', 'a', 't', 'w', 'o', 'r', 'k', '.', 'e', 'u', '\0',
   /* "byteshark.org", true */ 'b', 'y', 't', 'e', 's', 'h', 'a', 'r', 'k', '.', 'o', 'r', 'g', '\0',
   /* "byteshift.ca", true */ 'b', 'y', 't', 'e', 's', 'h', 'i', 'f', 't', '.', 'c', 'a', '\0',
   /* "bytesizedalex.com", true */ 'b', 'y', 't', 'e', 's', 'i', 'z', 'e', 'd', 'a', 'l', 'e', 'x', '.', 'c', 'o', 'm', '\0',
@@ -3319,32 +3321,29 @@ static const char kSTSHostTable[] = {
   /* "catharinesomerville.com", true */ 'c', 'a', 't', 'h', 'a', 'r', 'i', 'n', 'e', 's', 'o', 'm', 'e', 'r', 'v', 'i', 'l', 'l', 'e', '.', 'c', 'o', 'm', '\0',
   /* "catholics.dating", true */ 'c', 'a', 't', 'h', 'o', 'l', 'i', 'c', 's', '.', 'd', 'a', 't', 'i', 'n', 'g', '\0',
   /* "cathosa.nl", true */ 'c', 'a', 't', 'h', 'o', 's', 'a', '.', 'n', 'l', '\0',
   /* "cativa.net", true */ 'c', 'a', 't', 'i', 'v', 'a', '.', 'n', 'e', 't', '\0',
   /* "catmoose.ca", true */ 'c', 'a', 't', 'm', 'o', 'o', 's', 'e', '.', 'c', 'a', '\0',
   /* "catnet.dk", false */ 'c', 'a', 't', 'n', 'e', 't', '.', 'd', 'k', '\0',
   /* "catnmeow.com", true */ 'c', 'a', 't', 'n', 'm', 'e', 'o', 'w', '.', 'c', 'o', 'm', '\0',
   /* "catsmagic.pp.ua", true */ 'c', 'a', 't', 's', 'm', 'a', 'g', 'i', 'c', '.', 'p', 'p', '.', 'u', 'a', '\0',
-  /* "cattivo.nl", false */ 'c', 'a', 't', 't', 'i', 'v', 'o', '.', 'n', 'l', '\0',
   /* "caulfieldeastapartments.com.au", true */ 'c', 'a', 'u', 'l', 'f', 'i', 'e', 'l', 'd', 'e', 'a', 's', 't', 'a', 'p', 'a', 'r', 't', 'm', 'e', 'n', 't', 's', '.', 'c', 'o', 'm', '.', 'a', 'u', '\0',
   /* "caulfieldracecourseapartments.com.au", true */ 'c', 'a', 'u', 'l', 'f', 'i', 'e', 'l', 'd', 'r', 'a', 'c', 'e', 'c', 'o', 'u', 'r', 's', 'e', 'a', 'p', 'a', 'r', 't', 'm', 'e', 'n', 't', 's', '.', 'c', 'o', 'm', '.', 'a', 'u', '\0',
   /* "caulong-ao.net", true */ 'c', 'a', 'u', 'l', 'o', 'n', 'g', '-', 'a', 'o', '.', 'n', 'e', 't', '\0',
   /* "cav.ac", true */ 'c', 'a', 'v', '.', 'a', 'c', '\0',
-  /* "cavac.at", true */ 'c', 'a', 'v', 'a', 'c', '.', 'a', 't', '\0',
   /* "cavalierkingcharlesspaniel.com.br", true */ 'c', 'a', 'v', 'a', 'l', 'i', 'e', 'r', 'k', 'i', 'n', 'g', 'c', 'h', 'a', 'r', 'l', 'e', 's', 's', 'p', 'a', 'n', 'i', 'e', 'l', '.', 'c', 'o', 'm', '.', 'b', 'r', '\0',
   /* "cave-reynard.ch", true */ 'c', 'a', 'v', 'e', '-', 'r', 'e', 'y', 'n', 'a', 'r', 'd', '.', 'c', 'h', '\0',
   /* "cavern.tv", true */ 'c', 'a', 'v', 'e', 'r', 'n', '.', 't', 'v', '\0',
   /* "cavzodiaco.com.br", true */ 'c', 'a', 'v', 'z', 'o', 'd', 'i', 'a', 'c', 'o', '.', 'c', 'o', 'm', '.', 'b', 'r', '\0',
   /* "caylercapital.com", true */ 'c', 'a', 'y', 'l', 'e', 'r', 'c', 'a', 'p', 'i', 't', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "cazes.info", true */ 'c', 'a', 'z', 'e', 's', '.', 'i', 'n', 'f', 'o', '\0',
   /* "cbamo.org", true */ 'c', 'b', 'a', 'm', 'o', '.', 'o', 'r', 'g', '\0',
   /* "cbbank.com", true */ 'c', 'b', 'b', 'a', 'n', 'k', '.', 'c', 'o', 'm', '\0',
   /* "cbd.supply", true */ 'c', 'b', 'd', '.', 's', 'u', 'p', 'p', 'l', 'y', '\0',
-  /* "cbdev.de", true */ 'c', 'b', 'd', 'e', 'v', '.', 'd', 'e', '\0',
   /* "cbecrft.net", true */ 'c', 'b', 'e', 'c', 'r', 'f', 't', '.', 'n', 'e', 't', '\0',
   /* "cbintermountainrealty.com", true */ 'c', 'b', 'i', 'n', 't', 'e', 'r', 'm', 'o', 'u', 'n', 't', 'a', 'i', 'n', 'r', 'e', 'a', 'l', 't', 'y', '.', 'c', 'o', 'm', '\0',
   /* "ccac.gov", true */ 'c', 'c', 'a', 'c', '.', 'g', 'o', 'v', '\0',
   /* "ccayearbook.com", true */ 'c', 'c', 'a', 'y', 'e', 'a', 'r', 'b', 'o', 'o', 'k', '.', 'c', 'o', 'm', '\0',
   /* "ccgn.co", true */ 'c', 'c', 'g', 'n', '.', 'c', 'o', '\0',
   /* "ccja.ro", false */ 'c', 'c', 'j', 'a', '.', 'r', 'o', '\0',
   /* "ccl-sti.ch", true */ 'c', 'c', 'l', '-', 's', 't', 'i', '.', 'c', 'h', '\0',
   /* "ccsys.com", true */ 'c', 'c', 's', 'y', 's', '.', 'c', 'o', 'm', '\0',
@@ -3649,16 +3648,17 @@ static const char kSTSHostTable[] = {
   /* "chrisshort.net", false */ 'c', 'h', 'r', 'i', 's', 's', 'h', 'o', 'r', 't', '.', 'n', 'e', 't', '\0',
   /* "christadelphiananswers.org", true */ 'c', 'h', 'r', 'i', 's', 't', 'a', 'd', 'e', 'l', 'p', 'h', 'i', 'a', 'n', 'a', 'n', 's', 'w', 'e', 'r', 's', '.', 'o', 'r', 'g', '\0',
   /* "christadelphians.eu", true */ 'c', 'h', 'r', 'i', 's', 't', 'a', 'd', 'e', 'l', 'p', 'h', 'i', 'a', 'n', 's', '.', 'e', 'u', '\0',
   /* "christensenplace.us", true */ 'c', 'h', 'r', 'i', 's', 't', 'e', 'n', 's', 'e', 'n', 'p', 'l', 'a', 'c', 'e', '.', 'u', 's', '\0',
   /* "christiaanconover.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'a', 'n', 'c', 'o', 'n', 'o', 'v', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "christian-host.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', '-', 'h', 'o', 's', 't', '.', 'c', 'o', 'm', '\0',
   /* "christianforums.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'f', 'o', 'r', 'u', 'm', 's', '.', 'c', 'o', 'm', '\0',
   /* "christiangaetano.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'g', 'a', 'e', 't', 'a', 'n', 'o', '.', 'c', 'o', 'm', '\0',
+  /* "christianhoffmann.info", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'h', 'o', 'f', 'f', 'm', 'a', 'n', 'n', '.', 'i', 'n', 'f', 'o', '\0',
   /* "christianhospitaltank.org", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'h', 'o', 's', 'p', 'i', 't', 'a', 'l', 't', 'a', 'n', 'k', '.', 'o', 'r', 'g', '\0',
   /* "christianliebel.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'l', 'i', 'e', 'b', 'e', 'l', '.', 'c', 'o', 'm', '\0',
   /* "christianpusch.de", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 'p', 'u', 's', 'c', 'h', '.', 'd', 'e', '\0',
   /* "christians.dating", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 's', '.', 'd', 'a', 't', 'i', 'n', 'g', '\0',
   /* "christiansayswords.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 's', 'a', 'y', 's', 'w', 'o', 'r', 'd', 's', '.', 'c', 'o', 'm', '\0',
   /* "christianscholz.de", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 's', 'c', 'h', 'o', 'l', 'z', '.', 'd', 'e', '\0',
   /* "christianscholz.eu", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', 's', 'c', 'h', 'o', 'l', 'z', '.', 'e', 'u', '\0',
   /* "christiesantiques.com", true */ 'c', 'h', 'r', 'i', 's', 't', 'i', 'e', 's', 'a', 'n', 't', 'i', 'q', 'u', 'e', 's', '.', 'c', 'o', 'm', '\0',
@@ -3708,17 +3708,16 @@ static const char kSTSHostTable[] = {
   /* "cidbot.com", true */ 'c', 'i', 'd', 'b', 'o', 't', '.', 'c', 'o', 'm', '\0',
   /* "ciderclub.com", true */ 'c', 'i', 'd', 'e', 'r', 'c', 'l', 'u', 'b', '.', 'c', 'o', 'm', '\0',
   /* "cienbeaute-lidl.fr", true */ 'c', 'i', 'e', 'n', 'b', 'e', 'a', 'u', 't', 'e', '-', 'l', 'i', 'd', 'l', '.', 'f', 'r', '\0',
   /* "cieslar.io", true */ 'c', 'i', 'e', 's', 'l', 'a', 'r', '.', 'i', 'o', '\0',
   /* "cig-dem.com", true */ 'c', 'i', 'g', '-', 'd', 'e', 'm', '.', 'c', 'o', 'm', '\0',
   /* "cigar-cartel.com", true */ 'c', 'i', 'g', 'a', 'r', '-', 'c', 'a', 'r', 't', 'e', 'l', '.', 'c', 'o', 'm', '\0',
   /* "cigarterminal.com", false */ 'c', 'i', 'g', 'a', 'r', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "cigoteket.se", true */ 'c', 'i', 'g', 'o', 't', 'e', 'k', 'e', 't', '.', 's', 'e', '\0',
-  /* "ciicutini.ro", true */ 'c', 'i', 'i', 'c', 'u', 't', 'i', 'n', 'i', '.', 'r', 'o', '\0',
   /* "cima-idf.fr", true */ 'c', 'i', 'm', 'a', '-', 'i', 'd', 'f', '.', 'f', 'r', '\0',
   /* "cimballa.com", true */ 'c', 'i', 'm', 'b', 'a', 'l', 'l', 'a', '.', 'c', 'o', 'm', '\0',
   /* "cimfax.com", true */ 'c', 'i', 'm', 'f', 'a', 'x', '.', 'c', 'o', 'm', '\0',
   /* "cinartelorgu.com", true */ 'c', 'i', 'n', 'a', 'r', 't', 'e', 'l', 'o', 'r', 'g', 'u', '.', 'c', 'o', 'm', '\0',
   /* "cine-music.de", true */ 'c', 'i', 'n', 'e', '-', 'm', 'u', 's', 'i', 'c', '.', 'd', 'e', '\0',
   /* "cinefilzonen.se", true */ 'c', 'i', 'n', 'e', 'f', 'i', 'l', 'z', 'o', 'n', 'e', 'n', '.', 's', 'e', '\0',
   /* "cinema5.ru", true */ 'c', 'i', 'n', 'e', 'm', 'a', '5', '.', 'r', 'u', '\0',
   /* "cinemaclub.co", true */ 'c', 'i', 'n', 'e', 'm', 'a', 'c', 'l', 'u', 'b', '.', 'c', 'o', '\0',
@@ -3953,16 +3952,17 @@ static const char kSTSHostTable[] = {
   /* "codeine.co.uk", true */ 'c', 'o', 'd', 'e', 'i', 'n', 'e', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "codeit.guru", true */ 'c', 'o', 'd', 'e', 'i', 't', '.', 'g', 'u', 'r', 'u', '\0',
   /* "codeit.us", true */ 'c', 'o', 'd', 'e', 'i', 't', '.', 'u', 's', '\0',
   /* "codelove.de", true */ 'c', 'o', 'd', 'e', 'l', 'o', 'v', 'e', '.', 'd', 'e', '\0',
   /* "codenode.io", true */ 'c', 'o', 'd', 'e', 'n', 'o', 'd', 'e', '.', 'i', 'o', '\0',
   /* "codeplay.org", true */ 'c', 'o', 'd', 'e', 'p', 'l', 'a', 'y', '.', 'o', 'r', 'g', '\0',
   /* "codepoints.net", true */ 'c', 'o', 'd', 'e', 'p', 'o', 'i', 'n', 't', 's', '.', 'n', 'e', 't', '\0',
   /* "codepref.com", true */ 'c', 'o', 'd', 'e', 'p', 'r', 'e', 'f', '.', 'c', 'o', 'm', '\0',
+  /* "codepult.com", true */ 'c', 'o', 'd', 'e', 'p', 'u', 'l', 't', '.', 'c', 'o', 'm', '\0',
   /* "codera.co.uk", true */ 'c', 'o', 'd', 'e', 'r', 'a', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "codercross.com", true */ 'c', 'o', 'd', 'e', 'r', 'c', 'r', 'o', 's', 's', '.', 'c', 'o', 'm', '\0',
   /* "codereview.appspot.com", false */ 'c', 'o', 'd', 'e', 'r', 'e', 'v', 'i', 'e', 'w', '.', 'a', 'p', 'p', 's', 'p', 'o', 't', '.', 'c', 'o', 'm', '\0',
   /* "codereview.chromium.org", false */ 'c', 'o', 'd', 'e', 'r', 'e', 'v', 'i', 'e', 'w', '.', 'c', 'h', 'r', 'o', 'm', 'i', 'u', 'm', '.', 'o', 'r', 'g', '\0',
   /* "coderhangout.com", true */ 'c', 'o', 'd', 'e', 'r', 'h', 'a', 'n', 'g', 'o', 'u', 't', '.', 'c', 'o', 'm', '\0',
   /* "codesplain.in", true */ 'c', 'o', 'd', 'e', 's', 'p', 'l', 'a', 'i', 'n', '.', 'i', 'n', '\0',
   /* "codesport.io", true */ 'c', 'o', 'd', 'e', 's', 'p', 'o', 'r', 't', '.', 'i', 'o', '\0',
   /* "codestudies.net", true */ 'c', 'o', 'd', 'e', 's', 't', 'u', 'd', 'i', 'e', 's', '.', 'n', 'e', 't', '\0',
@@ -4194,16 +4194,17 @@ static const char kSTSHostTable[] = {
   /* "cookmedical.com", false */ 'c', 'o', 'o', 'k', 'm', 'e', 'd', 'i', 'c', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "cooko.at", true */ 'c', 'o', 'o', 'k', 'o', '.', 'a', 't', '\0',
   /* "cool-wallpapers.jp", true */ 'c', 'o', 'o', 'l', '-', 'w', 'a', 'l', 'l', 'p', 'a', 'p', 'e', 'r', 's', '.', 'j', 'p', '\0',
   /* "cool110.tk", true */ 'c', 'o', 'o', 'l', '1', '1', '0', '.', 't', 'k', '\0',
   /* "coolaj86.com", true */ 'c', 'o', 'o', 'l', 'a', 'j', '8', '6', '.', 'c', 'o', 'm', '\0',
   /* "cooldan.com", true */ 'c', 'o', 'o', 'l', 'd', 'a', 'n', '.', 'c', 'o', 'm', '\0',
   /* "coolerssr.space", true */ 'c', 'o', 'o', 'l', 'e', 'r', 's', 's', 'r', '.', 's', 'p', 'a', 'c', 'e', '\0',
   /* "coolgifs.de", true */ 'c', 'o', 'o', 'l', 'g', 'i', 'f', 's', '.', 'd', 'e', '\0',
+  /* "coolrc.me", true */ 'c', 'o', 'o', 'l', 'r', 'c', '.', 'm', 'e', '\0',
   /* "coolviewthermostat.com", true */ 'c', 'o', 'o', 'l', 'v', 'i', 'e', 'w', 't', 'h', 'e', 'r', 'm', 'o', 's', 't', 'a', 't', '.', 'c', 'o', 'm', '\0',
   /* "coopens.com", true */ 'c', 'o', 'o', 'p', 'e', 'n', 's', '.', 'c', 'o', 'm', '\0',
   /* "coore.jp", true */ 'c', 'o', 'o', 'r', 'e', '.', 'j', 'p', '\0',
   /* "copperhead.co", true */ 'c', 'o', 'p', 'p', 'e', 'r', 'h', 'e', 'a', 'd', '.', 'c', 'o', '\0',
   /* "copypoison.com", true */ 'c', 'o', 'p', 'y', 'p', 'o', 'i', 's', 'o', 'n', '.', 'c', 'o', 'm', '\0',
   /* "copyright-watch.org", true */ 'c', 'o', 'p', 'y', 'r', 'i', 'g', 'h', 't', '-', 'w', 'a', 't', 'c', 'h', '.', 'o', 'r', 'g', '\0',
   /* "copytrack.com", true */ 'c', 'o', 'p', 'y', 't', 'r', 'a', 'c', 'k', '.', 'c', 'o', 'm', '\0',
   /* "coramcdaniel.com", true */ 'c', 'o', 'r', 'a', 'm', 'c', 'd', 'a', 'n', 'i', 'e', 'l', '.', 'c', 'o', 'm', '\0',
@@ -4247,18 +4248,16 @@ static const char kSTSHostTable[] = {
   /* "cosmundi.de", true */ 'c', 'o', 's', 'm', 'u', 'n', 'd', 'i', '.', 'd', 'e', '\0',
   /* "cosplayer.com", true */ 'c', 'o', 's', 'p', 'l', 'a', 'y', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "costablancavoorjou.com", true */ 'c', 'o', 's', 't', 'a', 'b', 'l', 'a', 'n', 'c', 'a', 'v', 'o', 'o', 'r', 'j', 'o', 'u', '.', 'c', 'o', 'm', '\0',
   /* "costow.club", true */ 'c', 'o', 's', 't', 'o', 'w', '.', 'c', 'l', 'u', 'b', '\0',
   /* "costreportdata.com", true */ 'c', 'o', 's', 't', 'r', 'e', 'p', 'o', 'r', 't', 'd', 'a', 't', 'a', '.', 'c', 'o', 'm', '\0',
   /* "cotonea.de", true */ 'c', 'o', 't', 'o', 'n', 'e', 'a', '.', 'd', 'e', '\0',
   /* "cotwe-ge.ch", true */ 'c', 'o', 't', 'w', 'e', '-', 'g', 'e', '.', 'c', 'h', '\0',
   /* "cougar.dating", true */ 'c', 'o', 'u', 'g', 'a', 'r', '.', 'd', 'a', 't', 'i', 'n', 'g', '\0',
-  /* "coughlan.de", true */ 'c', 'o', 'u', 'g', 'h', 'l', 'a', 'n', '.', 'd', 'e', '\0',
-  /* "coumoul.fr", true */ 'c', 'o', 'u', 'm', 'o', 'u', 'l', '.', 'f', 'r', '\0',
   /* "counstellor.com", true */ 'c', 'o', 'u', 'n', 's', 't', 'e', 'l', 'l', 'o', 'r', '.', 'c', 'o', 'm', '\0',
   /* "counterglobal.com", true */ 'c', 'o', 'u', 'n', 't', 'e', 'r', 'g', 'l', 'o', 'b', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "countermail.com", true */ 'c', 'o', 'u', 'n', 't', 'e', 'r', 'm', 'a', 'i', 'l', '.', 'c', 'o', 'm', '\0',
   /* "countryattire.com", true */ 'c', 'o', 'u', 'n', 't', 'r', 'y', 'a', 't', 't', 'i', 'r', 'e', '.', 'c', 'o', 'm', '\0',
   /* "countryoutlaws.ca", true */ 'c', 'o', 'u', 'n', 't', 'r', 'y', 'o', 'u', 't', 'l', 'a', 'w', 's', '.', 'c', 'a', '\0',
   /* "countybankdel.com", true */ 'c', 'o', 'u', 'n', 't', 'y', 'b', 'a', 'n', 'k', 'd', 'e', 'l', '.', 'c', 'o', 'm', '\0',
   /* "couponcodeq.com", true */ 'c', 'o', 'u', 'p', 'o', 'n', 'c', 'o', 'd', 'e', 'q', '.', 'c', 'o', 'm', '\0',
   /* "couragefound.org", true */ 'c', 'o', 'u', 'r', 'a', 'g', 'e', 'f', 'o', 'u', 'n', 'd', '.', 'o', 'r', 'g', '\0',
@@ -4533,18 +4532,16 @@ static const char kSTSHostTable[] = {
   /* "customerbox.ir", true */ 'c', 'u', 's', 't', 'o', 'm', 'e', 'r', 'b', 'o', 'x', '.', 'i', 'r', '\0',
   /* "customfilmworks.com", true */ 'c', 'u', 's', 't', 'o', 'm', 'f', 'i', 'l', 'm', 'w', 'o', 'r', 'k', 's', '.', 'c', 'o', 'm', '\0',
   /* "customshort.link", true */ 'c', 'u', 's', 't', 'o', 'm', 's', 'h', 'o', 'r', 't', '.', 'l', 'i', 'n', 'k', '\0',
   /* "customwritings.com", true */ 'c', 'u', 's', 't', 'o', 'm', 'w', 'r', 'i', 't', 'i', 'n', 'g', 's', '.', 'c', 'o', 'm', '\0',
   /* "cutimbo.ovh", true */ 'c', 'u', 't', 'i', 'm', 'b', 'o', '.', 'o', 'v', 'h', '\0',
   /* "cuvva.co", true */ 'c', 'u', 'v', 'v', 'a', '.', 'c', 'o', '\0',
   /* "cuvva.co.uk", true */ 'c', 'u', 'v', 'v', 'a', '.', 'c', 'o', '.', 'u', 'k', '\0',
   /* "cuvva.com", true */ 'c', 'u', 'v', 'v', 'a', '.', 'c', 'o', 'm', '\0',
-  /* "cuvva.it", true */ 'c', 'u', 'v', 'v', 'a', '.', 'i', 't', '\0',
-  /* "cuvva.me", true */ 'c', 'u', 'v', 'v', 'a', '.', 'm', 'e', '\0',
   /* "cuvva.net", true */ 'c', 'u', 'v', 'v', 'a', '.', 'n', 'e', 't', '\0',
   /* "cuvva.uk", true */ 'c', 'u', 'v', 'v', 'a', '.', 'u', 'k', '\0',
   /* "cuvva.us", true */ 'c', 'u', 'v', 'v', 'a', '.', 'u', 's', '\0',
   /* "cve-le-carrousel.ch", true */ 'c', 'v', 'e', '-', 'l', 'e', '-', 'c', 'a', 'r', 'r', 'o', 'u', 's', 'e', 'l', '.', 'c', 'h', '\0',
   /* "cviip.ca", true */ 'c', 'v', 'i', 'i', 'p', '.', 'c', 'a', '\0',
   /* "cviip.com", true */ 'c', 'v', 'i', 'i', 'p', '.', 'c', 'o', 'm', '\0',
   /* "cvjm-memmingen.de", true */ 'c', 'v', 'j', 'm', '-', 'm', 'e', 'm', 'm', 'i', 'n', 'g', 'e', 'n', '.', 'd', 'e', '\0',
   /* "cvlibrary.co.uk", true */ 'c', 'v', 'l', 'i', 'b', 'r', 'a', 'r', 'y', '.', 'c', 'o', '.', 'u', 'k', '\0',
@@ -4758,16 +4755,17 @@ static const char kSTSHostTable[] = {
   /* "daropia.org", true */ 'd', 'a', 'r', 'o', 'p', 'i', 'a', '.', 'o', 'r', 'g', '\0',
   /* "darrenm.net", true */ 'd', 'a', 'r', 'r', 'e', 'n', 'm', '.', 'n', 'e', 't', '\0',
   /* "darrienworth.com", true */ 'd', 'a', 'r', 'r', 'i', 'e', 'n', 'w', 'o', 'r', 't', 'h', '.', 'c', 'o', 'm', '\0',
   /* "dart-tanke.com", true */ 'd', 'a', 'r', 't', '-', 't', 'a', 'n', 'k', 'e', '.', 'c', 'o', 'm', '\0',
   /* "dart-tanke.de", true */ 'd', 'a', 'r', 't', '-', 't', 'a', 'n', 'k', 'e', '.', 'd', 'e', '\0',
   /* "darth-sonic.de", true */ 'd', 'a', 'r', 't', 'h', '-', 's', 'o', 'n', 'i', 'c', '.', 'd', 'e', '\0',
   /* "dartsdon.jp", true */ 'd', 'a', 'r', 't', 's', 'd', 'o', 'n', '.', 'j', 'p', '\0',
   /* "darwinkel.net", false */ 'd', 'a', 'r', 'w', 'i', 'n', 'k', 'e', 'l', '.', 'n', 'e', 't', '\0',
+  /* "daryl.moe", true */ 'd', 'a', 'r', 'y', 'l', '.', 'm', 'o', 'e', '\0',
   /* "das-mediale-haus.de", true */ 'd', 'a', 's', '-', 'm', 'e', 'd', 'i', 'a', 'l', 'e', '-', 'h', 'a', 'u', 's', '.', 'd', 'e', '\0',
   /* "das-sommercamp.de", true */ 'd', 'a', 's', '-', 's', 'o', 'm', 'm', 'e', 'r', 'c', 'a', 'm', 'p', '.', 'd', 'e', '\0',
   /* "das-tyrol.at", true */ 'd', 'a', 's', '-', 't', 'y', 'r', 'o', 'l', '.', 'a', 't', '\0',
   /* "dash-board.jp", false */ 'd', 'a', 's', 'h', '-', 'b', 'o', 'a', 'r', 'd', '.', 'j', 'p', '\0',
   /* "dash.rocks", true */ 'd', 'a', 's', 'h', '.', 'r', 'o', 'c', 'k', 's', '\0',
   /* "dashboard.yt", true */ 'd', 'a', 's', 'h', 'b', 'o', 'a', 'r', 'd', '.', 'y', 't', '\0',
   /* "data.gov", true */ 'd', 'a', 't', 'a', '.', 'g', 'o', 'v', '\0',
   /* "data.haus", true */ 'd', 'a', 't', 'a', '.', 'h', 'a', 'u', 's', '\0',
@@ -5090,17 +5088,16 @@ static const char kSTSHostTable[] = {
   /* "detectefuite.ch", true */ 'd', 'e', 't', 'e', 'c', 't', 'e', 'f', 'u', 'i', 't', 'e', '.', 'c', 'h', '\0',
   /* "detectify.com", false */ 'd', 'e', 't', 'e', 'c', 't', 'i', 'f', 'y', '.', 'c', 'o', 'm', '\0',
   /* "dethemium.com", true */ 'd', 'e', 't', 'h', 'e', 'm', 'i', 'u', 'm', '.', 'c', 'o', 'm', '\0',
   /* "detoxsinutritie.ro", true */ 'd', 'e', 't', 'o', 'x', 's', 'i', 'n', 'u', 't', 'r', 'i', 't', 'i', 'e', '.', 'r', 'o', '\0',
   /* "detroit-english.de", true */ 'd', 'e', 't', 'r', 'o', 'i', 't', '-', 'e', 'n', 'g', 'l', 'i', 's', 'h', '.', 'd', 'e', '\0',
   /* "detroitstylepizza.com", true */ 'd', 'e', 't', 'r', 'o', 'i', 't', 's', 't', 'y', 'l', 'e', 'p', 'i', 'z', 'z', 'a', '.', 'c', 'o', 'm', '\0',
   /* "detskysad.com", true */ 'd', 'e', 't', 's', 'k', 'y', 's', 'a', 'd', '.', 'c', 'o', 'm', '\0',
   /* "detteflies.com", true */ 'd', 'e', 't', 't', 'e', 'f', 'l', 'i', 'e', 's', '.', 'c', 'o', 'm', '\0',
-  /* "detutorial.com", false */ 'd', 'e', 't', 'u', 't', 'o', 'r', 'i', 'a', 'l', '.', 'c', 'o', 'm', '\0',
   /* "deurenfabriek.nl", true */ 'd', 'e', 'u', 'r', 'e', 'n', 'f', 'a', 'b', 'r', 'i', 'e', 'k', '.', 'n', 'l', '\0',
   /* "deusu.de", true */ 'd', 'e', 'u', 's', 'u', '.', 'd', 'e', '\0',
   /* "deutsch-vietnamesisch-dolmetscher.com", true */ 'd', 'e', 'u', 't', 's', 'c', 'h', '-', 'v', 'i', 'e', 't', 'n', 'a', 'm', 'e', 's', 'i', 's', 'c', 'h', '-', 'd', 'o', 'l', 'm', 'e', 't', 's', 'c', 'h', 'e', 'r', '.', 'c', 'o', 'm', '\0',
   /* "dev-aegon.azurewebsites.net", true */ 'd', 'e', 'v', '-', 'a', 'e', 'g', 'o', 'n', '.', 'a', 'z', 'u', 'r', 'e', 'w', 'e', 'b', 's', 'i', 't', 'e', 's', '.', 'n', 'e', 't', '\0',
   /* "dev-tek.de", true */ 'd', 'e', 'v', '-', 't', 'e', 'k', '.', 'd', 'e', '\0',
   /* "devb.nl", true */ 'd', 'e', 'v', 'b', '.', 'n', 'l', '\0',
   /* "devct.cz", true */ 'd', 'e', 'v', 'c', 't', '.', 'c', 'z', '\0',
   /* "devdesco.com", true */ 'd', 'e', 'v', 'd', 'e', 's', 'c', 'o', '.', 'c', 'o', 'm', '\0',
@@ -5264,16 +5261,17 @@ static const char kSTSHostTable[] = {
   /* "diju.ch", true */ 'd', 'i', 'j', 'u', '.', 'c', 'h', '\0',
   /* "dilichen.fr", true */ 'd', 'i', 'l', 'i', 'c', 'h', 'e', 'n', '.', 'f', 'r', '\0',
   /* "dillewijnzwapak.nl", true */ 'd', 'i', 'l', 'l', 'e', 'w', 'i', 'j', 'n', 'z', 'w', 'a', 'p', 'a', 'k', '.', 'n', 'l', '\0',
   /* "dillonkorman.com", true */ 'd', 'i', 'l', 'l', 'o', 'n', 'k', 'o', 'r', 'm', 'a', 'n', '.', 'c', 'o', 'm', '\0',
   /* "dimanss47.net", true */ 'd', 'i', 'm', 'a', 'n', 's', 's', '4', '7', '.', 'n', 'e', 't', '\0',
   /* "dime-staging.com", true */ 'd', 'i', 'm', 'e', '-', 's', 't', 'a', 'g', 'i', 'n', 'g', '.', 'c', 'o', 'm', '\0',
   /* "dime.io", true */ 'd', 'i', 'm', 'e', '.', 'i', 'o', '\0',
   /* "dimez.ru", true */ 'd', 'i', 'm', 'e', 'z', '.', 'r', 'u', '\0',
+  /* "dimonb.com", true */ 'd', 'i', 'm', 'o', 'n', 'b', '.', 'c', 'o', 'm', '\0',
   /* "dinepont.fr", true */ 'd', 'i', 'n', 'e', 'p', 'o', 'n', 't', '.', 'f', 'r', '\0',
   /* "dingcc.com", true */ 'd', 'i', 'n', 'g', 'c', 'c', '.', 'c', 'o', 'm', '\0',
   /* "dinge.xyz", true */ 'd', 'i', 'n', 'g', 'e', '.', 'x', 'y', 'z', '\0',
   /* "dingss.com", true */ 'd', 'i', 'n', 'g', 's', 's', '.', 'c', 'o', 'm', '\0',
   /* "dinmtb.dk", true */ 'd', 'i', 'n', 'm', 't', 'b', '.', 'd', 'k', '\0',
   /* "dino.li", true */ 'd', 'i', 'n', 'o', '.', 'l', 'i', '\0',
   /* "dinotv.at", true */ 'd', 'i', 'n', 'o', 't', 'v', '.', 'a', 't', '\0',
   /* "dintillat.fr", true */ 'd', 'i', 'n', 't', 'i', 'l', 'l', 'a', 't', '.', 'f', 'r', '\0',
@@ -5480,17 +5478,16 @@ static const char kSTSHostTable[] = {
   /* "donotlink.it", true */ 'd', 'o', 'n', 'o', 't', 'l', 'i', 'n', 'k', '.', 'i', 't', '\0',
   /* "donotspellitgav.in", true */ 'd', 'o', 'n', 'o', 't', 's', 'p', 'e', 'l', 'l', 'i', 't', 'g', 'a', 'v', '.', 'i', 'n', '\0',
   /* "donsbach-edv.de", true */ 'd', 'o', 'n', 's', 'b', 'a', 'c', 'h', '-', 'e', 'd', 'v', '.', 'd', 'e', '\0',
   /* "dontbubble.me", true */ 'd', 'o', 'n', 't', 'b', 'u', 'b', 'b', 'l', 'e', '.', 'm', 'e', '\0',
   /* "dontcageus.org", true */ 'd', 'o', 'n', 't', 'c', 'a', 'g', 'e', 'u', 's', '.', 'o', 'r', 'g', '\0',
   /* "donthedragonwilson.com", true */ 'd', 'o', 'n', 't', 'h', 'e', 'd', 'r', 'a', 'g', 'o', 'n', 'w', 'i', 'l', 's', 'o', 'n', '.', 'c', 'o', 'm', '\0',
   /* "dooby.fr", true */ 'd', 'o', 'o', 'b', 'y', '.', 'f', 'r', '\0',
   /* "doobydude.us", true */ 'd', 'o', 'o', 'b', 'y', 'd', 'u', 'd', 'e', '.', 'u', 's', '\0',
-  /* "doodledraw.ninja", true */ 'd', 'o', 'o', 'd', 'l', 'e', 'd', 'r', 'a', 'w', '.', 'n', 'i', 'n', 'j', 'a', '\0',
   /* "dooleylabs.com", true */ 'd', 'o', 'o', 'l', 'e', 'y', 'l', 'a', 'b', 's', '.', 'c', 'o', 'm', '\0',
   /* "dooleytackaberry.com", true */ 'd', 'o', 'o', 'l', 'e', 'y', 't', 'a', 'c', 'k', 'a', 'b', 'e', 'r', 'r', 'y', '.', 'c', 'o', 'm', '\0',
   /* "doomsworld.com", true */ 'd', 'o', 'o', 'm', 's', 'w', 'o', 'r', 'l', 'd', '.', 'c', 'o', 'm', '\0',
   /* "doordecor.bg", true */ 'd', 'o', 'o', 'r', 'd', 'e', 'c', 'o', 'r', '.', 'b', 'g', '\0',
   /* "doorflow.com", true */ 'd', 'o', 'o', 'r', 'f', 'l', 'o', 'w', '.', 'c', 'o', 'm', '\0',
   /* "dopesoft.de", true */ 'd', 'o', 'p', 'e', 's', 'o', 'f', 't', '.', 'd', 'e', '\0',
   /* "dopfer-fenstertechnik.de", true */ 'd', 'o', 'p', 'f', 'e', 'r', '-', 'f', 'e', 'n', 's', 't', 'e', 'r', 't', 'e', 'c', 'h', 'n', 'i', 'k', '.', 'd', 'e', '\0',
   /* "dopost.it", false */ 'd', 'o', 'p', 'o', 's', 't', '.', 'i', 't', '\0',
@@ -5786,16 +5783,17 @@ static const char kSTSHostTable[] = {
   /* "eam-gmbh.com", true */ 'e', 'a', 'm', '-', 'g', 'm', 'b', 'h', '.', 'c', 'o', 'm', '\0',
   /* "eames-clayton.us", true */ 'e', 'a', 'm', 'e', 's', '-', 'c', 'l', 'a', 'y', 't', 'o', 'n', '.', 'u', 's', '\0',
   /* "earl.org.uk", true */ 'e', 'a', 'r', 'l', '.', 'o', 'r', 'g', '.', 'u', 'k', '\0',
   /* "earlyyearshub.com", true */ 'e', 'a', 'r', 'l', 'y', 'y', 'e', 'a', 'r', 's', 'h', 'u', 'b', '.', 'c', 'o', 'm', '\0',
   /* "earmarks.gov", true */ 'e', 'a', 'r', 'm', 'a', 'r', 'k', 's', '.', 'g', 'o', 'v', '\0',
   /* "earth-people.org", true */ 'e', 'a', 'r', 't', 'h', '-', 'p', 'e', 'o', 'p', 'l', 'e', '.', 'o', 'r', 'g', '\0',
   /* "earticleblog.com", true */ 'e', 'a', 'r', 't', 'i', 'c', 'l', 'e', 'b', 'l', 'o', 'g', '.', 'c', 'o', 'm', '\0',
   /* "earvinkayonga.com", true */ 'e', 'a', 'r', 'v', 'i', 'n', 'k', 'a', 'y', 'o', 'n', 'g', 'a', '.', 'c', 'o', 'm', '\0',
+  /* "eason-yang.com", true */ 'e', 'a', 's', 'o', 'n', '-', 'y', 'a', 'n', 'g', '.', 'c', 'o', 'm', '\0',
   /* "eastarm.net", true */ 'e', 'a', 's', 't', 'a', 'r', 'm', '.', 'n', 'e', 't', '\0',
   /* "eastmanbusinessinstitute.com", true */ 'e', 'a', 's', 't', 'm', 'a', 'n', 'b', 'u', 's', 'i', 'n', 'e', 's', 's', 'i', 'n', 's', 't', 'i', 't', 'u', 't', 'e', '.', 'c', 'o', 'm', '\0',
   /* "eastmontgroup.com", true */ 'e', 'a', 's', 't', 'm', 'o', 'n', 't', 'g', 'r', 'o', 'u', 'p', '.', 'c', 'o', 'm', '\0',
   /* "easy-rpg.org", false */ 'e', 'a', 's', 'y', '-', 'r', 'p', 'g', '.', 'o', 'r', 'g', '\0',
   /* "easyconstat.com", true */ 'e', 'a', 's', 'y', 'c', 'o', 'n', 's', 't', 'a', 't', '.', 'c', 'o', 'm', '\0',
   /* "easycosmetic.ch", true */ 'e', 'a', 's', 'y', 'c', 'o', 's', 'm', 'e', 't', 'i', 'c', '.', 'c', 'h', '\0',
   /* "easycup.com", true */ 'e', 'a', 's', 'y', 'c', 'u', 'p', '.', 'c', 'o', 'm', '\0',
   /