Bug 659067 - Part 3: Only show network errors for user triggered sync, not for scheduled ones. r=philikon
authorMarina Samuel <msamuel@mozilla.com>
Fri, 26 Aug 2011 14:01:35 -0700
changeset 76306 4778b8babec66927817f5161ac8545c5b65bc3f4
parent 76305 bdd0d53dcaff2e0616b08086aa575de4697fe24c
child 76307 62177ac1ccad18d8ce59a737ce22b4515fa554e7
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
reviewersphilikon
bugs659067
milestone9.0a1
Bug 659067 - Part 3: Only show network errors for user triggered sync, not for scheduled ones. r=philikon
browser/base/content/browser-syncui.js
services/sync/locales/en-US/sync.properties
services/sync/modules/constants.js
services/sync/modules/policies.js
services/sync/services-sync.js
services/sync/tests/unit/test_errorhandler.js
services/sync/tests/unit/test_errorhandler_filelog.js
services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
services/sync/tests/unit/test_service_login.js
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -35,28 +35,27 @@
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 // gSyncUI handles updating the tools menu
 let gSyncUI = {
-  _obs: ["weave:engine:sync:error",
-         "weave:service:sync:start",
-         "weave:service:sync:finish",
-         "weave:service:sync:error",
+  _obs: ["weave:service:sync:start",
          "weave:service:sync:delayed",
          "weave:service:quota:remaining",
          "weave:service:setup-complete",
          "weave:service:login:start",
          "weave:service:login:finish",
-         "weave:service:login:error",
          "weave:service:logout:finish",
-         "weave:service:start-over"],
+         "weave:service:start-over",
+         "weave:ui:login:error",
+         "weave:ui:sync:error",
+         "weave:ui:sync:finish"],
 
   _unloaded: false,
 
   init: function SUI_init() {
     // Proceed to set up the UI if Sync has already started up.
     // Otherwise we'll do it when Sync is firing up.
     if (Weave.Status.ready) {
       this.initUI();
@@ -185,24 +184,16 @@ let gSyncUI = {
 
     let button = document.getElementById("sync-button");
     if (!button)
       return;
 
     button.setAttribute("status", "active");
   },
 
-  onSyncFinish: function SUI_onSyncFinish() {
-    this._onSyncEnd(true);
-  },
-
-  onSyncError: function SUI_onSyncError() {
-    this._onSyncEnd(false);
-  },
-
   onSyncDelay: function SUI_onSyncDelay() {
     // basically, we want to just inform users that stuff is going to take a while
     let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
     let description = this._stringBundle.GetStringFromName("error.sync.no_node_found");
     let buttons = [new Weave.NotificationButton(
       this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
       this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
       function() { gSyncUI.openServerStatus(); return true; }
@@ -221,25 +212,36 @@ let gSyncUI = {
     this.updateUI();
   },
 
   onLoginError: function SUI_onLoginError() {
     // if login fails, any other notifications are essentially moot
     Weave.Notifications.removeAll();
 
     // if we haven't set up the client, don't show errors
-    if (this._needsSetup() || Weave.ErrorHandler.shouldIgnoreError()) {
+    if (this._needsSetup()) {
       this.updateUI();
       return;
     }
 
     let title = this._stringBundle.GetStringFromName("error.login.title");
-    let reason = Weave.Utils.getErrorString(Weave.Status.login);
-    let description =
-      this._stringBundle.formatStringFromName("error.login.description", [reason], 1);
+
+    let description;
+    if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
+      // Convert to days
+      let lastSync =
+        Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
+      description =
+        this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
+    } else {
+      let reason = Weave.Utils.getErrorString(Weave.Status.login);
+      description =
+        this._stringBundle.formatStringFromName("error.sync.description", [reason], 1);
+    }
+
     let buttons = [];
     buttons.push(new Weave.NotificationButton(
       this._stringBundle.GetStringFromName("error.login.prefs.label"),
       this._stringBundle.GetStringFromName("error.login.prefs.accesskey"),
       function() { gSyncUI.openPrefs(); return true; }
     ));
 
     let notification = new Weave.Notification(title, description, null,
@@ -274,17 +276,17 @@ let gSyncUI = {
 
   openServerStatus: function () {
     let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
     window.openUILinkIn(statusURL, "tab");
   },
 
   // Commands
   doSync: function SUI_doSync() {
-    setTimeout(function() Weave.Service.sync(), 0);
+    setTimeout(function() Weave.ErrorHandler.syncAndReportErrors(), 0);
   },
 
   handleToolbarButton: function SUI_handleStatusbarButton() {
     if (this._needsSetup())
       this.openSetup();
     else
       this.doSync();
   },
@@ -338,89 +340,100 @@ let gSyncUI = {
     // Show the day-of-week and time (HH:MM) of last sync
     let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M");
     let lastSyncLabel =
       this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1);
 
     syncButton.setAttribute("tooltiptext", lastSyncLabel);
   },
 
-  _onSyncEnd: function SUI__onSyncEnd(success) {
+  onSyncFinish: function SUI_onSyncFinish() {
     let title = this._stringBundle.GetStringFromName("error.sync.title");
-    if (!success) {
-      if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
-        this.onLoginError();
-        return;
-      }
+
+    // Clear out sync failures on a successful sync
+    Weave.Notifications.removeAll(title);
 
-      // Ignore network related errors unless we haven't been able to
-      // sync for a while.
-      if (Weave.ErrorHandler.shouldIgnoreError()) {
-        this.updateUI();
-        return;
-      }
+    if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) {
+      title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
+      Weave.Notifications.removeAll(title);
+      this._wasDelayed = false;
+    }
 
-      let error = Weave.Utils.getErrorString(Weave.Status.sync);
-      let description =
-        this._stringBundle.formatStringFromName("error.sync.description", [error], 1);
+    this.updateUI();
+  },
 
-      let priority = Weave.Notifications.PRIORITY_WARNING;
-      let buttons = [];
+  onSyncError: function SUI_onSyncError() {
+    let title = this._stringBundle.GetStringFromName("error.sync.title");
 
-      // Check if the client is outdated in some way
-      let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE;
-      for (let [engine, reason] in Iterator(Weave.Status.engines))
-        outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE;
+    if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
+      this.onLoginError();
+      return;
+    }
 
-      if (outdated) {
-        description = this._stringBundle.GetStringFromName(
-          "error.sync.needUpdate.description");
-        buttons.push(new Weave.NotificationButton(
-          this._stringBundle.GetStringFromName("error.sync.needUpdate.label"),
-          this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"),
-          function() { window.openUILinkIn("https://services.mozilla.com/update/", "tab"); return true; }
-        ));
-      }
-      else if (Weave.Status.sync == Weave.OVER_QUOTA) {
-        description = this._stringBundle.GetStringFromName(
-          "error.sync.quota.description");
-        buttons.push(new Weave.NotificationButton(
-          this._stringBundle.GetStringFromName(
-            "error.sync.viewQuotaButton.label"),
-          this._stringBundle.GetStringFromName(
-            "error.sync.viewQuotaButton.accesskey"),
-          function() { gSyncUI.openQuotaDialog(); return true; } )
-        );
-      }
-      else if (Weave.Status.enforceBackoff) {
-        priority = Weave.Notifications.PRIORITY_INFO;
-        buttons.push(new Weave.NotificationButton(
-          this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
-          this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
-          function() { gSyncUI.openServerStatus(); return true; }
-        ));
-      }
-      else {
-        priority = Weave.Notifications.PRIORITY_INFO;
-        buttons.push(new Weave.NotificationButton(
-          this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
-          this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
-          function() { gSyncUI.doSync(); return true; }
-        ));
-      }
+    let description;
+    if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
+      // Convert to days
+      let lastSync =
+        Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
+      description =
+        this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
+    } else {
+      let error = Weave.Utils.getErrorString(Weave.Status.sync);
+      description =
+        this._stringBundle.formatStringFromName("error.sync.description", [error], 1);
+    }
+    let priority = Weave.Notifications.PRIORITY_WARNING;
+    let buttons = [];
+
+    // Check if the client is outdated in some way
+    let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE;
+    for (let [engine, reason] in Iterator(Weave.Status.engines))
+      outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE;
 
-      let notification =
-        new Weave.Notification(title, description, null, priority, buttons);
-      Weave.Notifications.replaceTitle(notification);
+    if (outdated) {
+      description = this._stringBundle.GetStringFromName(
+        "error.sync.needUpdate.description");
+      buttons.push(new Weave.NotificationButton(
+        this._stringBundle.GetStringFromName("error.sync.needUpdate.label"),
+        this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"),
+        function() { window.openUILinkIn("https://services.mozilla.com/update/", "tab"); return true; }
+      ));
+    }
+    else if (Weave.Status.sync == Weave.OVER_QUOTA) {
+      description = this._stringBundle.GetStringFromName(
+        "error.sync.quota.description");
+      buttons.push(new Weave.NotificationButton(
+        this._stringBundle.GetStringFromName(
+          "error.sync.viewQuotaButton.label"),
+        this._stringBundle.GetStringFromName(
+          "error.sync.viewQuotaButton.accesskey"),
+        function() { gSyncUI.openQuotaDialog(); return true; } )
+      );
+    }
+    else if (Weave.Status.enforceBackoff) {
+      priority = Weave.Notifications.PRIORITY_INFO;
+      buttons.push(new Weave.NotificationButton(
+        this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
+        this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
+        function() { gSyncUI.openServerStatus(); return true; }
+      ));
     }
     else {
-      // Clear out sync failures on a successful sync
-      Weave.Notifications.removeAll(title);
+      priority = Weave.Notifications.PRIORITY_INFO;
+      buttons.push(new Weave.NotificationButton(
+        this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
+        this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
+        function() { gSyncUI.doSync(); return true; }
+      ));
     }
 
+    let notification =
+      new Weave.Notification(title, description, null, priority, buttons);
+    Weave.Notifications.replaceTitle(notification);
+
     if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) {
       title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
       Weave.Notifications.removeAll(title);
       this._wasDelayed = false;
     }
 
     this.updateUI();
   },
@@ -430,21 +443,20 @@ let gSyncUI = {
       Cu.reportError("SyncUI observer called after unload: " + topic);
       return;
     }
 
     switch (topic) {
       case "weave:service:sync:start":
         this.onActivityStart();
         break;
-      case "weave:service:sync:finish":
+      case "weave:ui:sync:finish":
         this.onSyncFinish();
         break;
-      case "weave:engine:sync:error":
-      case "weave:service:sync:error":
+      case "weave:ui:sync:error":
         this.onSyncError();
         break;
       case "weave:service:sync:delayed":
         this.onSyncDelay();
         break;
       case "weave:service:quota:remaining":
         this.onQuotaNotice();
         break;
@@ -452,17 +464,17 @@ let gSyncUI = {
         this.onLoginFinish();
         break;
       case "weave:service:login:start":
         this.onActivityStart();
         break;
       case "weave:service:login:finish":
         this.onLoginFinish();
         break;
-      case "weave:service:login:error":
+      case "weave:ui:login:error":
         this.onLoginError();
         break;
       case "weave:service:logout:finish":
         this.onLogout();
         break;
       case "weave:service:start-over":
         this.onStartOver();
         break;
--- a/services/sync/locales/en-US/sync.properties
+++ b/services/sync/locales/en-US/sync.properties
@@ -15,16 +15,17 @@ error.login.title = Error While Signing 
 error.login.description = Sync encountered an error while connecting: %1$S.  Please try again.
 error.login.prefs.label = Preferences…
 error.login.prefs.accesskey = P
 # should decide if we're going to show this
 error.logout.title = Error While Signing Out
 error.logout.description = Sync encountered an error while connecting.  It's probably ok, and you don't have to do anything about it.
 error.sync.title = Error While Syncing
 error.sync.description = Sync encountered an error while syncing: %1$S.  Sync will automatically retry this action.
+error.sync.prolonged_failure = Sync has not been able to complete during the last %1$S days. Please check your network settings.
 error.sync.no_node_found = The Sync server is a little busy right now, but you don't need to do anything about it.  We'll start syncing your data as soon as we can!
 error.sync.no_node_found.title = Sync Delay
 error.sync.serverStatusButton.label = Server Status
 error.sync.serverStatusButton.accesskey = V
 error.sync.needUpdate.description = You need to update Firefox Sync to continue syncing your data.
 error.sync.needUpdate.label = Update Firefox Sync
 error.sync.needUpdate.accesskey = U
 error.sync.tryAgainButton.label = Sync Now
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -157,16 +157,17 @@ LOGIN_FAILED_LOGIN_REJECTED:           "
 METARECORD_DOWNLOAD_FAIL:              "error.sync.reason.metarecord_download_fail",
 VERSION_OUT_OF_DATE:                   "error.sync.reason.version_out_of_date",
 DESKTOP_VERSION_OUT_OF_DATE:           "error.sync.reason.desktop_version_out_of_date",
 SETUP_FAILED_NO_PASSPHRASE:            "error.sync.reason.setup_failed_no_passphrase",
 CREDENTIALS_CHANGED:                   "error.sync.reason.credentials_changed",
 ABORT_SYNC_COMMAND:                    "aborting sync, process commands said so",
 NO_SYNC_NODE_FOUND:                    "error.sync.reason.no_node_found",
 OVER_QUOTA:                            "error.sync.reason.over_quota",
+PROLONGED_SYNC_FAILURE:                "error.sync.prolonged_failure",
 
 RESPONSE_OVER_QUOTA:                   "14",
 
 // engine failure status codes
 ENGINE_UPLOAD_FAIL:                    "error.engine.reason.record_upload_fail",
 ENGINE_DOWNLOAD_FAIL:                  "error.engine.reason.record_download_fail",
 ENGINE_UNKNOWN_FAIL:                   "error.engine.reason.unknown_fail",
 ENGINE_APPLY_FAIL:                     "error.engine.reason.apply_fail",
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -92,17 +92,17 @@ let SyncScheduler = {
     Svc.Obs.add("weave:service:login:error", this);
     Svc.Obs.add("weave:service:logout:finish", this);
     Svc.Obs.add("weave:service:sync:error", this);
     Svc.Obs.add("weave:service:backoff:interval", this);
     Svc.Obs.add("weave:service:ready", this);
     Svc.Obs.add("weave:engine:sync:applied", this);
     Svc.Obs.add("weave:service:setup-complete", this);
     Svc.Obs.add("weave:service:start-over", this);
-				
+
     if (Status.checkSetup() == STATUS_OK) {
       Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
     }
 
   },
 
   observe: function observe(subject, topic, data) {
     switch(topic) {
@@ -154,30 +154,30 @@ let SyncScheduler = {
         // subject is the exception thrown by an engine's sync() method
         let exception = subject;
         if (exception.status >= 500 && exception.status <= 504) {
           this.requiresBackoff = true;
         }
         break;
       case "weave:service:login:error":
         this.clearSyncTriggers();
-        
+
         // Try again later, just as if we threw an error... only without the
         // error count.
         if (Status.login == MASTER_PASSWORD_LOCKED) {
           this._log.debug("Couldn't log in: master password is locked.");
           this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL");
           this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
         }
         break;
       case "weave:service:logout:finish":
         // Start or cancel the sync timer depending on if
         // logged in or logged out
         this.checkSyncStatus();
-        break; 
+        break;
       case "weave:service:sync:error":
         // There may be multiple clients but if the sync fails, client mode
         // should still be updated so that the next sync has a correct interval.
         this.updateClientMode();
         this.adjustSyncInterval();
         this.handleSyncError();
         break;
       case "weave:service:backoff:interval":
@@ -191,17 +191,17 @@ let SyncScheduler = {
         let delay = Svc.Prefs.get("autoconnectDelay");
         if (delay) {
           this.delayedAutoConnect(delay);
         }
         break;
       case "weave:engine:sync:applied":
         let numItems = subject.applied;
         this._log.trace("Engine " + data + " applied " + numItems + " items.");
-        if (numItems) 
+        if (numItems)
           this.hasIncomingItems = true;
         break;
       case "weave:service:setup-complete":
          Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
          break;
       case "weave:service:start-over":
          Svc.Idle.removeIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
          SyncScheduler.setDefaults();
@@ -261,17 +261,17 @@ let SyncScheduler = {
     this.checkSyncStatus();
   },
 
   /**
    * Process the locally stored clients list to figure out what mode to be in
    */
   updateClientMode: function updateClientMode() {
     // Nothing to do if it's the same amount
-    let {numClients} = Clients.stats;	
+    let numClients = Clients.stats.numClients;
     if (this.numClients == numClients)
       return;
 
     this._log.debug("Client count: " + this.numClients + " -> " + numClients);
     this.numClients = numClients;
 
     if (numClients <= 1) {
       this._log.trace("Adjusting syncThreshold to SINGLE_USER_THRESHOLD");
@@ -303,17 +303,17 @@ let SyncScheduler = {
       this._log.debug("Global Score threshold hit, triggering sync.");
       wait = 0;
     }
     this.scheduleNextSync(wait);
   },
 
   /**
    * Call sync() if Master Password is not locked.
-   * 
+   *
    * Otherwise, reschedule a sync for later.
    */
   syncIfMPUnlocked: function syncIfMPUnlocked() {
     // No point if we got kicked out by the master password dialog.
     if (Status.login == MASTER_PASSWORD_LOCKED &&
         Utils.mpLocked()) {
       this._log.debug("Not initiating sync: Login status is " + Status.login);
 
@@ -428,29 +428,34 @@ let SyncScheduler = {
 
 };
 
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 
 let ErrorHandler = {
 
+  /**
+   * Flag that turns on error reporting for all errors, incl. network errors.
+   */
+  dontIgnoreErrors: false,
+
   init: function init() {
     Svc.Obs.add("weave:engine:sync:applied", this);
     Svc.Obs.add("weave:engine:sync:error", this);
     Svc.Obs.add("weave:service:login:error", this);
     Svc.Obs.add("weave:service:sync:error", this);
     Svc.Obs.add("weave:service:sync:finish", this);
 
     this.initLogs();
   },
 
   initLogs: function initLogs() {
     this._log = Log4Moz.repository.getLogger("Sync.ErrorHandler");
-		this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
+    this._log.level = Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
 
     let root = Log4Moz.repository.getLogger("Sync");
     root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")];
 
     let formatter = new Log4Moz.BasicFormatter();
     let capp = new Log4Moz.ConsoleAppender(formatter);
     capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")];
     root.addAppender(capp);
@@ -480,58 +485,74 @@ let ErrorHandler = {
         let engine_name = data;   // engine name that threw the exception
 
         this.checkServerError(exception);
 
         Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL];
         this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
         break;
       case "weave:service:login:error":
-        if (Status.login == LOGIN_FAILED_NETWORK_ERROR &&
-            !Services.io.offline) {
-          this._ignorableErrorCount += 1;
-        } else {
+        if (this.shouldReportError()) {
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                             LOG_PREFIX_ERROR);
+          Svc.Obs.notify("weave:ui:login:error");
         }
+
+        this.dontIgnoreErrors = false;
         break;
       case "weave:service:sync:error":
-        switch (Status.sync) {
-          case LOGIN_FAILED_NETWORK_ERROR:
-            if (!Services.io.offline) {
-              this._ignorableErrorCount += 1;
-            }
-            break;
-          case CREDENTIALS_CHANGED:
-            Weave.Service.logout();
-            break;
-          default:
-            this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
-                              LOG_PREFIX_ERROR);
-            break;
+        if (Status.sync == CREDENTIALS_CHANGED) {
+          Weave.Service.logout();
         }
+
+        if (this.shouldReportError()) {
+          this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
+                            LOG_PREFIX_ERROR);
+          Svc.Obs.notify("weave:ui:sync:error");
+        } else {
+          Svc.Obs.notify("weave:ui:sync:finish");
+        }
+
+        this.dontIgnoreErrors = false;
         break;
       case "weave:service:sync:finish":
+        this.dontIgnoreErrors = false;
+
         if (Status.service == SYNC_FAILED_PARTIAL) {
           this._log.debug("Some engines did not sync correctly.");
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"),
                             LOG_PREFIX_ERROR);
+
+          if (this.shouldReportError()) {
+            Svc.Obs.notify("weave:ui:sync:error");
+            break;
+          }
         } else {
           this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"),
                             LOG_PREFIX_SUCCESS);
         }
-        this._ignorableErrorCount = 0;
+        Svc.Obs.notify("weave:ui:sync:finish");
         break;
     }
   },
 
   /**
+   * Trigger a sync and don't muffle any errors, particularly network errors.
+   */
+  syncAndReportErrors: function syncAndReportErrors() {
+    this._log.debug("Beginning user-triggered sync.");
+
+    this.dontIgnoreErrors = true;
+    Utils.nextTick(Weave.Service.sync, Weave.Service);
+  },
+
+  /**
    * Generate a log file for the sync that just completed
    * and refresh the input & output streams.
-   * 
+   *
    * @param flushToFile
    *        the log file to be flushed/reset
    *
    * @param filenamePrefix
    *        a value of either LOG_PREFIX_SUCCESS or LOG_PREFIX_ERROR
    *        to be used as the log filename prefix
    */
   resetFileLog: function resetFileLog(flushToFile, filenamePrefix) {
@@ -550,17 +571,17 @@ let ErrorHandler = {
       }
     } else {
       Svc.Obs.notify("weave:service:reset-file-log");
     }
   },
 
   /**
    * Translates server error codes to meaningful strings.
-   * 
+   *
    * @param code
    *        server error code as an integer
    */
   errorStr: function errorStr(code) {
     switch (code.toString()) {
     case "1":
       return "illegal-method";
     case "2":
@@ -579,22 +600,30 @@ let ErrorHandler = {
       return "invalid-record";
     case "9":
       return "weak-password";
     default:
       return "generic-server-error";
     }
   },
 
-  _ignorableErrorCount: 0,
-  shouldIgnoreError: function shouldIgnoreError() {
-    // Never show an error bar for a locked master password.
-    return (Status.login == MASTER_PASSWORD_LOCKED) ||
-           ([Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) != -1
-            && this._ignorableErrorCount < MAX_IGNORE_ERROR_COUNT);
+  shouldReportError: function shouldReportError() {
+    if (Status.login == MASTER_PASSWORD_LOCKED) {
+      return false;
+    }
+
+    let lastSync = Svc.Prefs.get("lastSync");
+    if (lastSync && ((Date.now() - Date.parse(lastSync)) >
+        Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) {
+      Status.sync = PROLONGED_SYNC_FAILURE;
+      return true;
+    }
+
+    return (this.dontIgnoreErrors ||
+            [Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1);
   },
 
   /**
    * Handle HTTP response results or exceptions and set the appropriate
    * Status.* bits.
    */
   checkServerError: function checkServerError(resp) {
     switch (resp.status) {
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -10,16 +10,18 @@ pref("services.sync.lastversion", "first
 pref("services.sync.sendVersionInfo", true);
 
 pref("services.sync.scheduler.singleDeviceInterval", 3600);  // 1 hour
 pref("services.sync.scheduler.idleInterval",         3600);  // 1 hour
 pref("services.sync.scheduler.activeInterval",       300);   // 5 minutes
 pref("services.sync.scheduler.immediateInterval",    60);    // 1 minute
 pref("services.sync.scheduler.idleTime",             300);   // 5 minutes
 
+pref("services.sync.errorhandler.networkFailureReportTimeout", 604800); // 1 week
+
 pref("services.sync.engine.bookmarks", true);
 pref("services.sync.engine.history", true);
 pref("services.sync.engine.passwords", true);
 pref("services.sync.engine.prefs", true);
 pref("services.sync.engine.tabs", true);
 pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$");
 
 pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -8,16 +8,27 @@ Cu.import("resource://services-sync/stat
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 
+const PROLONGED_ERROR_DURATION =
+  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
+
+const NON_PROLONGED_ERROR_DURATION =
+  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
+
+function setLastSync(lastSyncValue) {
+  Svc.Prefs.set("lastSync", (new Date(Date.now() -
+    lastSyncValue)).toString());
+}
+
 function CatapultEngine() {
   SyncEngine.call(this, "Catapult");
 }
 CatapultEngine.prototype = {
   __proto__: SyncEngine.prototype,
   exception: null, // tests fill this in
   sync: function sync() {
     throw this.exception;
@@ -31,16 +42,26 @@ function run_test() {
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
 
   run_next_test();
 }
 
+function generateCredentialsChangedFailure() {
+  // Make sync fail due to changed credentials. We simply re-encrypt
+  // the keys with a different Sync Key, without changing the local one.
+  let newSyncKeyBundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, Service.username);
+  newSyncKeyBundle.keyStr = "23456234562345623456234562";
+  let keys = CollectionKeys.asWBO();
+  keys.encrypt(newSyncKeyBundle);
+  keys.upload(Service.cryptoKeysURL);
+}
+
 function sync_httpd_setup() {
   let global = new ServerWBO("global", {
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
     engines: {clients: {version: Clients.version,
                         syncID: Clients.syncID}}
   });
   let clientsColl = new ServerCollection({}, true);
@@ -81,17 +102,17 @@ add_test(function test_401_logout() {
   // By calling sync, we ensure we're logged in.
   Service.sync();
   do_check_eq(Status.sync, SYNC_SUCCEEDED);
   do_check_true(Service.isLoggedIn);
 
   // Make sync fail due to login rejected.
   Service.username = "janedoe";
   Service.sync();
-  
+
   do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
   do_check_false(Service.isLoggedIn);
 
   // Clean up.
   Service.startOver();
   server.stop(run_next_test);
 });
 
@@ -99,55 +120,516 @@ add_test(function test_credentials_chang
   let server = sync_httpd_setup();
   setUp();
 
   // By calling sync, we ensure we're logged in.
   Service.sync();
   do_check_eq(Status.sync, SYNC_SUCCEEDED);
   do_check_true(Service.isLoggedIn);
 
-  // Make sync fail due to changed credentials. We simply re-encrypt
-  // the keys with a different Sync Key, without changing the local one.
-  let newSyncKeyBundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, Service.username);
-  newSyncKeyBundle.keyStr = "23456234562345623456234562";
-  let keys = CollectionKeys.asWBO();
-  keys.encrypt(newSyncKeyBundle);
-  keys.upload(Service.cryptoKeysURL);
+  generateCredentialsChangedFailure();
   Service.sync();
-  
+
   do_check_eq(Status.sync, CREDENTIALS_CHANGED);
   do_check_false(Service.isLoggedIn);
 
   // Clean up.
   Service.startOver();
   server.stop(run_next_test);
 });
 
-add_test(function test_shouldIgnoreError() {
+add_test(function test_no_lastSync_pref() {
+  // Test reported error.
+  Status.resetSync();
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test unreported error.
+  Status.resetSync();
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  run_next_test();
+});
+
+add_test(function test_shouldReportError() {
   Status.login = MASTER_PASSWORD_LOCKED;
+  do_check_false(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
   Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-
-  // Error ignored since master password locked.
-  do_check_true(ErrorHandler.shouldIgnoreError());
+  do_check_true(ErrorHandler.shouldReportError());
 
-  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-  Status.sync = LOGIN_FAILED_NETWORK_ERROR
+  // Test dontIgnoreErrors, network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = true;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
 
-  // Error ignored due to network error.
-  do_check_true(ErrorHandler.shouldIgnoreError());
+  // Test non-network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test non-network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
 
-  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-  Status.sync = NO_SYNC_NODE_FOUND;
+  // Test network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test non-network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(ErrorHandler.shouldReportError());
 
-  // Error not ignored.
-  do_check_false(ErrorHandler.shouldIgnoreError());
+  // Test non-network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(ErrorHandler.shouldReportError());
+
+  // Test network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_false(ErrorHandler.shouldReportError());
+
+  // Test network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.dontIgnoreErrors = false;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_false(ErrorHandler.shouldReportError());
 
   run_next_test();
 });
 
+add_test(function test_shouldReportError_master_password() {
+  _("Test error ignored due to locked master password");
+  let server = sync_httpd_setup();
+  setUp();
+
+  // Monkey patch Service.verifyLogin to imitate
+  // master password being locked.
+  Service._verifyLogin = Service.verifyLogin;
+  Service.verifyLogin = function () {
+    Status.login = MASTER_PASSWORD_LOCKED;
+    return false;
+  };
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+  do_check_false(ErrorHandler.shouldReportError());
+
+  // Clean up.
+  Service.verifyLogin = Service._verifyLogin;
+  Service.startOver();
+  server.stop(run_next_test);
+});
+
+add_test(function test_login_syncAndReportErrors_non_network_error() {
+  // Test non-network errors are reported
+  // when calling syncAndReportErrors
+  let server = sync_httpd_setup();
+  setUp();
+  Service.password = "";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_sync_syncAndReportErrors_non_network_error() {
+  // Test non-network errors are reported
+  // when calling syncAndReportErrors
+  let server = sync_httpd_setup();
+  setUp();
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  generateCredentialsChangedFailure();
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_login_syncAndReportErrors_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are
+  // reported when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+  Service.password = "";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_sync_syncAndReportErrors_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are
+  // reported when calling syncAndReportErrors.
+  let server = sync_httpd_setup();
+  setUp();
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  generateCredentialsChangedFailure();
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_login_syncAndReportErrors_network_error() {
+  // Test network errors are reported when calling syncAndReportErrors.
+  Service.username = "johndoe";
+  Service.password = "ilovejane";
+  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  Service.clusterURL = "http://localhost:8080/";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
+
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+
+add_test(function test_sync_syncAndReportErrors_network_error() {
+  // Test network errors are reported when calling syncAndReportErrors.
+  Services.io.offline = true;
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+
+    Services.io.offline = false;
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_login_syncAndReportErrors_prolonged_network_error() {
+  // Test prolonged, network errors are reported
+  // when calling syncAndReportErrors.
+  Service.username = "johndoe";
+  Service.password = "ilovejane";
+  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  Service.clusterURL = "http://localhost:8080/";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
+  // Test prolonged, network errors are reported
+  // when calling syncAndReportErrors.
+  Services.io.offline = true;
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Services.io.offline = false;
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  ErrorHandler.syncAndReportErrors();
+});
+
+add_test(function test_login_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are reported
+  let server = sync_httpd_setup();
+  setUp();
+  Service.password = "";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_sync_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are reported
+  let server = sync_httpd_setup();
+  setUp();
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  generateCredentialsChangedFailure();
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_login_prolonged_network_error() {
+  // Test prolonged, network errors are reported
+  Service.username = "johndoe";
+  Service.password = "ilovejane";
+  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  Service.clusterURL = "http://localhost:8080/";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_sync_prolonged_network_error() {
+  // Test prolonged, network errors are reported
+  Services.io.offline = true;
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
+
+    Services.io.offline = false;
+    Service.startOver();
+    run_next_test();
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_login_non_network_error() {
+  // Test non-network errors are reported
+  let server = sync_httpd_setup();
+  setUp();
+  Service.password = "";
+
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+
+add_test(function test_sync_non_network_error() {
+  // Test non-network errors are reported
+  let server = sync_httpd_setup();
+  setUp();
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  generateCredentialsChangedFailure();
+
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+
+    Service.startOver();
+    server.stop(run_next_test);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_login_network_error() {
+  Service.username = "johndoe";
+  Service.password = "ilovejane";
+  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  Service.clusterURL = "http://localhost:8080/";
+
+  Svc.Obs.add("weave:ui:login:error", function() {
+    do_throw("Should not get here!");
+  });
+
+  // Test network errors are not reported.
+  Svc.Obs.add("weave:service:login:error", function onUIUpdate() {
+    Svc.Obs.remove("weave:service:login:error", onUIUpdate);
+
+    // Wait until other login:error observers are called since
+    // it may change Status.sync.
+    Utils.nextTick(function() {
+      do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
+
+      Service.startOver();
+      Services.io.offline = false;
+      run_next_test();
+    });
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
+add_test(function test_sync_network_error() {
+  // Test network errors are not reported.
+  Services.io.offline = true;
+
+  Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
+    Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
+    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
+
+    Service.startOver();
+    Services.io.offline = false;
+    Status.resetSync();
+    run_next_test();
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+});
+
 add_test(function test_sync_engine_generic_fail() {
   let server = sync_httpd_setup();
 
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.sync = function sync() {
     Svc.Obs.notify("weave:engine:sync:error", "", "steam");
   };
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -4,16 +4,24 @@
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/log4moz.js");
 
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 
+const PROLONGED_ERROR_DURATION =
+  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
+
+function setLastSync(lastSyncValue) {
+  Svc.Prefs.set("lastSync", (new Date(Date.now() -
+    lastSyncValue)).toString());
+}
+
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_noOutput() {
   // Clear log output from startup.
   Svc.Prefs.set("log.appender.file.logOnSuccess", false);
   Svc.Obs.notify("weave:service:sync:finish");
@@ -110,17 +118,18 @@ add_test(function test_sync_error_logOnE
     Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
     // No log file was written.
     do_check_false(logsdir.directoryEntries.hasMoreElements());
 
     Svc.Prefs.resetBranch("");
     run_next_test();
   });
 
-  // Fake an unsuccessful sync.
+  // Fake an unsuccessful sync due to prolonged failure.
+  setLastSync(PROLONGED_ERROR_DURATION);
   Svc.Obs.notify("weave:service:sync:error");
 });
 
 add_test(function test_sync_error_logOnError_true() {
   Svc.Prefs.set("log.appender.file.logOnError", true);
 
   let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
   const MESSAGE = "this WILL show up";
@@ -151,17 +160,18 @@ add_test(function test_sync_error_logOnE
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
       run_next_test();
     });
   });
 
-  // Fake an unsuccessful sync.
+  // Fake an unsuccessful sync due to prolonged failure.
+  setLastSync(PROLONGED_ERROR_DURATION);
   Svc.Obs.notify("weave:service:sync:error");
 });
 
 add_test(function test_login_error_logOnError_false() {
   Svc.Prefs.set("log.appender.file.logOnError", false);
 
   let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
   log.info("this won't show up");
@@ -170,17 +180,18 @@ add_test(function test_login_error_logOn
     Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
     // No log file was written.
     do_check_false(logsdir.directoryEntries.hasMoreElements());
 
     Svc.Prefs.resetBranch("");
     run_next_test();
   });
 
-  // Fake an unsuccessful login.
+  // Fake an unsuccessful login due to prolonged failure.
+  setLastSync(PROLONGED_ERROR_DURATION);
   Svc.Obs.notify("weave:service:login:error");
 });
 
 add_test(function test_login_error_logOnError_true() {
   Svc.Prefs.set("log.appender.file.logOnError", true);
 
   let log = Log4Moz.repository.getLogger("Sync.Test.FileLog");
   const MESSAGE = "this WILL show up";
@@ -211,11 +222,12 @@ add_test(function test_login_error_logOn
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
       run_next_test();
     });
   });
 
-  // Fake an unsuccessful sync.
+  // Fake an unsuccessful login due to prolonged failure.
+  setLastSync(PROLONGED_ERROR_DURATION);
   Svc.Obs.notify("weave:service:login:error");
 });
--- a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
@@ -150,85 +150,53 @@ add_test(function test_overQuota() {
   server.stop(run_next_test);
 });
 
 add_test(function test_service_networkError() {
   _("Test: Connection refused error from Service.sync() leads to the right status code.");
   setUp();
   // Provoke connection refused.
   Service.clusterURL = "http://localhost:12345/";
-  ErrorHandler._ignorableErrorCount = 0;
 
   try {
     do_check_eq(Status.sync, SYNC_SUCCEEDED);
 
     Service._loggedIn = true;
     Service.sync();
 
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-    do_check_eq(ErrorHandler._ignorableErrorCount, 1);
     do_check_eq(Status.service, SYNC_FAILED);
   } finally {
     Status.resetSync();
     Service.startOver();
   }
   run_next_test();
 });
 
 add_test(function test_service_offline() {
   _("Test: Wanting to sync in offline mode leads to the right status code but does not increment the ignorable error count.");
   setUp();
   Services.io.offline = true;
-  ErrorHandler._ignorableErrorCount = 0;
 
   try {
     do_check_eq(Status.sync, SYNC_SUCCEEDED);
 
     Service._loggedIn = true;
     Service.sync();
 
     do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-    do_check_eq(ErrorHandler._ignorableErrorCount, 0);
     do_check_eq(Status.service, SYNC_FAILED);
   } finally {
     Status.resetSync();
     Service.startOver();
   }
   Services.io.offline = false;
   run_next_test();
 });
 
-add_test(function test_service_reset_ignorableErrorCount() {
-  _("Test: Successful sync resets the ignorable error count.");
-  setUp();
-  let server = sync_httpd_setup();
-  ErrorHandler._ignorableErrorCount = 10;
-
-  // Disable the engine so that sync completes.
-  let engine = Engines.get("catapult");
-  engine.enabled = false;
-
-  try {
-    do_check_eq(Status.sync, SYNC_SUCCEEDED);
-
-    do_check_true(generateAndUploadKeys());
-
-    Service.login();
-    Service.sync();
-
-    do_check_eq(Status.sync, SYNC_SUCCEEDED);
-    do_check_eq(ErrorHandler._ignorableErrorCount, 0);
-    do_check_eq(Status.service, STATUS_OK);
-  } finally {
-    Status.resetSync();
-    Service.startOver();
-  }
-  server.stop(run_next_test);
-});
-
 add_test(function test_engine_networkError() {
   _("Test: Network related exceptions from engine.sync() lead to the right status code.");
   setUp();
   let server = sync_httpd_setup();
 
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.exception = Components.Exception("NS_ERROR_UNKNOWN_HOST",
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -24,20 +24,18 @@ function run_test() {
 
   run_next_test();
 }
 
 add_test(function test_offline() {
   try {
     _("The right bits are set when we're offline.");
     Services.io.offline = true;
-    do_check_eq(ErrorHandler._ignorableErrorCount, 0);
     do_check_false(!!Service.login());
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-    do_check_eq(ErrorHandler._ignorableErrorCount, 0);
     Services.io.offline = false;
   } finally {
     Svc.Prefs.resetBranch("");
     run_next_test();
   }
 });
 
 function setup() {