Bug 1530346 - Upgrade kinto-offline-client and report IndexedDB errors properly r=glasserc
authorMathieu Leplatre <mathieu@mozilla.com>
Tue, 26 Feb 2019 16:15:05 +0000
changeset 519070 d48af3b7c76df42e0f66e818a73a0720ff07f092
parent 519069 6e4eb864eeb65002d6e7f8632dc65b40be838859
child 519071 14dc79650c63e7054014f6f8166e0cccbbe2d0b5
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglasserc
bugs1530346
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1530346 - Upgrade kinto-offline-client and report IndexedDB errors properly r=glasserc - Upgrade kinto-offline-client to v12.3.0 - Make sure IndexedDB errors are not reported as unknown errors Differential Revision: https://phabricator.services.mozilla.com/D21208
services/common/kinto-offline-client.js
services/common/kinto-storage-adapter.js
services/common/tests/unit/test_kinto.js
services/settings/RemoteSettingsClient.jsm
services/settings/test/unit/test_remote_settings.js
toolkit/components/extensions/ExtensionStorageSync.jsm
--- a/services/common/kinto-offline-client.js
+++ b/services/common/kinto-offline-client.js
@@ -28,17 +28,17 @@
 //
 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
 // more details.
 const global = this;
 
 var EXPORTED_SYMBOLS = ["Kinto"];
 
 /*
- * Version 12.2.4 - 8fb687a
+ * Version 12.3.0 - f7a9e81
  */
 
 (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Kinto = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
 /*
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -560,17 +560,17 @@ class IDB extends _base.default {
     super();
     this.cid = cid;
     this.dbName = options.dbName || "KintoDB";
     this._options = options;
     this._db = null;
   }
 
   _handleError(method, err) {
-    const error = new Error(method + "() " + err.message);
+    const error = new Error(`IndexedDB ${method}() ${err.message}`);
     error.stack = err.stack;
     throw error;
   }
   /**
    * Ensures a connection to the IndexedDB database has been opened.
    *
    * @override
    * @return {Promise}
@@ -609,17 +609,17 @@ class IDB extends _base.default {
       }
     });
 
     if (dataToMigrate) {
       const {
         records,
         timestamp
       } = dataToMigrate;
-      await this.loadDump(records);
+      await this.importBulk(records);
       await this.saveLastModified(timestamp);
       console.log(`${this.cid}: data was migrated successfully.`); // Delete the old database.
 
       await deleteDatabase(this.cid);
       console.warn(`${this.cid}: old database was deleted.`);
     }
 
     return this;
@@ -881,23 +881,36 @@ class IDB extends _base.default {
       return entry ? entry.value : null;
     } catch (e) {
       this._handleError("getLastModified", e);
     }
   }
   /**
    * Load a dump of records exported from a server.
    *
+   * @deprecated Use {@link importBulk} instead.
    * @abstract
    * @param  {Array} records The records to load.
    * @return {Promise}
    */
 
 
   async loadDump(records) {
+    return this.importBulk(records);
+  }
+  /**
+   * Load records in bulk that were exported from a server.
+   *
+   * @abstract
+   * @param  {Array} records The records to load.
+   * @return {Promise}
+   */
+
+
+  async importBulk(records) {
     try {
       await this.execute(transaction => {
         // Since the put operations are asynchronous, we chain
         // them together. The last one will be waited for the
         // `transaction.oncomplete` callback. (see #execute())
         let i = 0;
         putNext();
 
@@ -915,17 +928,17 @@ class IDB extends _base.default {
       const lastModified = Math.max(...records.map(record => record.last_modified));
 
       if (lastModified > previousLastModified) {
         await this.saveLastModified(lastModified);
       }
 
       return records;
     } catch (e) {
-      this._handleError("loadDump", e);
+      this._handleError("importBulk", e);
     }
   }
 
 }
 /**
  * IDB transaction proxy.
  *
  * @param  {IDB} adapter        The call IDB adapter
@@ -1112,18 +1125,31 @@ class BaseAdapter {
    * @return {Promise}
    */
 
 
   getLastModified() {
     throw new Error("Not Implemented.");
   }
   /**
+   * Load records in bulk that were exported from a server.
+   *
+   * @abstract
+   * @param  {Array} records The records to load.
+   * @return {Promise}
+   */
+
+
+  importBulk(records) {
+    throw new Error("Not Implemented.");
+  }
+  /**
    * Load a dump of records exported from a server.
    *
+   * @deprecated Use {@link importBulk} instead.
    * @abstract
    * @param  {Array} records The records to load.
    * @return {Promise}
    */
 
 
   loadDump(records) {
     throw new Error("Not Implemented.");
@@ -1135,17 +1161,17 @@ exports.default = BaseAdapter;
 
 },{}],6:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.recordsEqual = recordsEqual;
-exports.CollectionTransaction = exports.default = exports.SyncResultObject = void 0;
+exports.CollectionTransaction = exports.default = exports.ServerWasFlushedError = exports.SyncResultObject = void 0;
 
 var _base = _interopRequireDefault(require("./adapters/base"));
 
 var _IDB = _interopRequireDefault(require("./adapters/IDB"));
 
 var _utils = require("./utils");
 
 var _uuid = require("uuid");
@@ -1258,16 +1284,32 @@ class SyncResultObject {
     this.ok = this.errors.length + this.conflicts.length === 0;
     return this;
   }
 
 }
 
 exports.SyncResultObject = SyncResultObject;
 
+class ServerWasFlushedError extends Error {
+  constructor(clientTimestamp, serverTimestamp, message) {
+    super(message);
+
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, ServerWasFlushedError);
+    }
+
+    this.clientTimestamp = clientTimestamp;
+    this.serverTimestamp = serverTimestamp;
+  }
+
+}
+
+exports.ServerWasFlushedError = ServerWasFlushedError;
+
 function createUUIDSchema() {
   return {
     generate() {
       return (0, _uuid.v4)();
     },
 
     validate(id) {
       return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
@@ -2257,17 +2299,18 @@ class Collection {
     // This is relevant for the Kinto demo server
     // (and thus for many new comers).
 
     const localSynced = options.lastModified;
     const serverChanged = unquoted > options.lastModified;
     const emptyCollection = data.length === 0;
 
     if (!options.exclude && localSynced && serverChanged && emptyCollection) {
-      throw Error("Server has been flushed.");
+      const e = new ServerWasFlushedError(localSynced, unquoted, "Server has been flushed. Client Side Timestamp: " + localSynced + " Server Side Timestamp: " + unquoted);
+      throw e;
     }
 
     syncResultObject.lastModified = unquoted; // Decode incoming changes.
 
     const decodedChanges = await Promise.all(data.map(change => {
       return this._decodeRecord("remote", change);
     })); // Hook receives decoded records.
 
@@ -2573,22 +2616,37 @@ class Collection {
     return result;
   }
   /**
    * Load a list of records already synced with the remote server.
    *
    * The local records which are unsynced or whose timestamp is either missing
    * or superior to those being loaded will be ignored.
    *
+   * @deprecated Use {@link importBulk} instead.
    * @param  {Array} records The previously exported list of records to load.
    * @return {Promise} with the effectively imported records.
    */
 
 
   async loadDump(records) {
+    return this.importBulk(records);
+  }
+  /**
+   * Load a list of records already synced with the remote server.
+   *
+   * The local records which are unsynced or whose timestamp is either missing
+   * or superior to those being loaded will be ignored.
+   *
+   * @param  {Array} records The previously exported list of records to load.
+   * @return {Promise} with the effectively imported records.
+   */
+
+
+  async importBulk(records) {
     if (!Array.isArray(records)) {
       throw new Error("Records is not an array.");
     }
 
     for (const record of records) {
       if (!record.hasOwnProperty("id") || !this.idSchema.validate(record.id)) {
         throw new Error("Record has invalid ID: " + JSON.stringify(record));
       }
@@ -2614,17 +2672,17 @@ class Collection {
       const localRecord = existingById[record.id];
       const shouldKeep = // No local record with this id.
       localRecord === undefined || // Or local record is synced
       localRecord._status === "synced" && // And was synced from server
       localRecord.last_modified !== undefined && // And is older than imported one.
       record.last_modified > localRecord.last_modified;
       return shouldKeep;
     });
-    return await this.db.loadDump(newRecords.map(markSynced));
+    return await this.db.importBulk(newRecords.map(markSynced));
   }
 
 }
 /**
  * A Collection-oriented wrapper for an adapter's transaction.
  *
  * This defines the high-level functions available on a collection.
  * The collection itself offers functions of the same name. These will
--- a/services/common/kinto-storage-adapter.js
+++ b/services/common/kinto-storage-adapter.js
@@ -326,27 +326,31 @@ class FirefoxAdapter extends Kinto.adapt
       return records;
     }).then(results => {
       // The resulting list of records is filtered and sorted.
       // XXX: with some efforts, this could be implemented using SQL.
       return reduceRecords(params.filters, params.order, results);
     });
   }
 
+  async loadDump(records) {
+    return this.importBulk(records);
+  }
+
   /**
    * Load a list of records into the local database.
    *
    * Note: The adapter is not in charge of filtering the already imported
    * records. This is done in `Collection#loadDump()`, as a common behaviour
    * between every adapters.
    *
    * @param  {Array} records.
    * @return {Array} imported records.
    */
-  async loadDump(records) {
+  async importBulk(records) {
     const connection = this._connection;
     const collection_name = this.collection;
     await connection.executeTransaction(async function doImport() {
       for (let record of records) {
         const params = {
           collection_name,
           record_id: record.id,
           record: JSON.stringify(record),
--- a/services/common/tests/unit/test_kinto.js
+++ b/services/common/tests/unit/test_kinto.js
@@ -208,24 +208,24 @@ add_task(async function test_kinto_list(
     }
   } finally {
     await sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
-add_task(async function test_loadDump_ignores_already_imported_records() {
+add_task(async function test_importBulk_ignores_already_imported_records() {
   let sqliteHandle;
   try {
     sqliteHandle = await do_get_kinto_sqliteHandle();
     const collection = do_get_kinto_collection(sqliteHandle);
     const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
-    await collection.loadDump([record]);
-    let impactedRecords = await collection.loadDump([record]);
+    await collection.importBulk([record]);
+    let impactedRecords = await collection.importBulk([record]);
     Assert.equal(impactedRecords.length, 0);
   } finally {
     await sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
--- a/services/settings/RemoteSettingsClient.jsm
+++ b/services/settings/RemoteSettingsClient.jsm
@@ -380,17 +380,17 @@ class RemoteSettingsClient extends Event
           await this.emit("sync", { data: filteredSyncResult });
         } catch (e) {
           reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
           throw e;
         }
       }
     } catch (e) {
       // IndexedDB errors. See https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest/error
-      if (/(AbortError|ConstraintError|QuotaExceededError|VersionError)/.test(e.message)) {
+      if (/(IndexedDB|AbortError|ConstraintError|QuotaExceededError|VersionError)/.test(e.message)) {
         reportStatus = UptakeTelemetry.STATUS.CUSTOM_1_ERROR;
       }
       // No specific error was tracked, mark it as unknown.
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
       }
       throw e;
     } finally {
--- a/services/settings/test/unit/test_remote_settings.js
+++ b/services/settings/test/unit/test_remote_settings.js
@@ -380,16 +380,33 @@ add_task(async function test_telemetry_r
 
   client.openCollection = backup;
   const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
   const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 add_task(clear_state);
 
+add_task(async function test_telemetry_reports_indexeddb_as_custom_1() {
+  const backup = client.openCollection;
+  const msg = "IndexedDB getLastModified() The operation failed for reasons unrelated to the database itself";
+  client.openCollection = () => { throw new Error(msg); };
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+
+  try {
+    await client.maybeSync(2000);
+  } catch (e) { }
+
+  client.openCollection = backup;
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.CUSTOM_1_ERROR]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
 add_task(async function test_bucketname_changes_when_bucket_pref_changes() {
   equal(client.bucketName, "main");
 
   Services.prefs.setCharPref("services.settings.default_bucket", "main-preview");
 
   equal(client.bucketName, "main-preview");
 });
 add_task(clear_state);
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -1052,17 +1052,17 @@ class ExtensionStorageSync {
       }
       // No conflicts, or conflict was just someone else adding keys.
       return result;
     } catch (e) {
       if (KeyRingEncryptionRemoteTransformer.isOutdatedKB(e) ||
           e instanceof ServerKeyringDeleted ||
           // This is another way that ServerKeyringDeleted can
           // manifest; see bug 1350088 for more details.
-          e.message == "Server has been flushed.") {
+          e.message.includes("Server has been flushed.")) {
         // Check if our token is still valid, or if we got locked out
         // between starting the sync and talking to Kinto.
         const isSessionValid = await this._fxaService.sessionStatus();
         if (isSessionValid) {
           log.error("Couldn't decipher old keyring; deleting the default bucket and resetting sync status");
           await this._deleteBucket();
           await this.cryptoCollection.resetSyncStatus();