diff options
Diffstat (limited to 'services/sync/modules/healthreport.jsm')
-rw-r--r-- | services/sync/modules/healthreport.jsm | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/services/sync/modules/healthreport.jsm b/services/sync/modules/healthreport.jsm new file mode 100644 index 000000000..47161c095 --- /dev/null +++ b/services/sync/modules/healthreport.jsm @@ -0,0 +1,262 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "SyncProvider", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Metrics.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}; +const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT}; +const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER}; + +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); + +function SyncMeasurement1() { + Metrics.Measurement.call(this); +} + +SyncMeasurement1.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "sync", + version: 1, + + fields: { + enabled: DAILY_LAST_NUMERIC_FIELD, + preferredProtocol: DAILY_LAST_TEXT_FIELD, + activeProtocol: DAILY_LAST_TEXT_FIELD, + syncStart: DAILY_COUNTER_FIELD, + syncSuccess: DAILY_COUNTER_FIELD, + syncError: DAILY_COUNTER_FIELD, + }, +}); + +function SyncDevicesMeasurement1() { + Metrics.Measurement.call(this); +} + +SyncDevicesMeasurement1.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "devices", + version: 1, + + fields: {}, + + shouldIncludeField: function (name) { + return true; + }, + + fieldType: function (name) { + return Metrics.Storage.FIELD_DAILY_COUNTER; + }, +}); + +function SyncMigrationMeasurement1() { + Metrics.Measurement.call(this); +} + +SyncMigrationMeasurement1.prototype = Object.freeze({ + __proto__: Metrics.Measurement.prototype, + + name: "migration", + version: 1, + + fields: { + state: DAILY_LAST_TEXT_FIELD, // last "user" or "internal" state we saw for the day + accepted: DAILY_COUNTER_FIELD, // number of times user tried to start migration + declined: DAILY_COUNTER_FIELD, // number of times user closed nagging infobar + unlinked: DAILY_LAST_NUMERIC_FIELD, // did the user decline and unlink + }, +}); + +this.SyncProvider = function () { + Metrics.Provider.call(this); +}; +SyncProvider.prototype = Object.freeze({ + __proto__: Metrics.Provider.prototype, + + name: "org.mozilla.sync", + + measurementTypes: [ + SyncDevicesMeasurement1, + SyncMeasurement1, + SyncMigrationMeasurement1, + ], + + _OBSERVERS: [ + "weave:service:sync:start", + "weave:service:sync:finish", + "weave:service:sync:error", + "fxa-migration:state-changed", + "fxa-migration:internal-state-changed", + "fxa-migration:internal-telemetry", + ], + + postInit: function () { + for (let o of this._OBSERVERS) { + Services.obs.addObserver(this, o, false); + } + + return Promise.resolve(); + }, + + onShutdown: function () { + for (let o of this._OBSERVERS) { + Services.obs.removeObserver(this, o); + } + + return Promise.resolve(); + }, + + observe: function (subject, topic, data) { + switch (topic) { + case "weave:service:sync:start": + case "weave:service:sync:finish": + case "weave:service:sync:error": + return this._observeSync(subject, topic, data); + + case "fxa-migration:state-changed": + case "fxa-migration:internal-state-changed": + case "fxa-migration:internal-telemetry": + return this._observeMigration(subject, topic, data); + } + Cu.reportError("unexpected topic in sync healthreport provider: " + topic); + }, + + _observeSync: function (subject, topic, data) { + let field; + switch (topic) { + case "weave:service:sync:start": + field = "syncStart"; + break; + + case "weave:service:sync:finish": + field = "syncSuccess"; + break; + + case "weave:service:sync:error": + field = "syncError"; + break; + + default: + Cu.reportError("unexpected sync topic in sync healthreport provider: " + topic); + return; + } + + let m = this.getMeasurement(SyncMeasurement1.prototype.name, + SyncMeasurement1.prototype.version); + return this.enqueueStorageOperation(function recordSyncEvent() { + return m.incrementDailyCounter(field); + }); + }, + + _observeMigration: function(subject, topic, data) { + switch (topic) { + case "fxa-migration:state-changed": + case "fxa-migration:internal-state-changed": { + // We record both "user" and "internal" states in the same field. This + // works for us as user state is always null when there is an internal + // state. + if (!data) { + return; // we don't count the |null| state + } + let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name, + SyncMigrationMeasurement1.prototype.version); + return this.enqueueStorageOperation(function() { + return m.setDailyLastText("state", data); + }); + } + + case "fxa-migration:internal-telemetry": { + // |data| is our field name. + let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name, + SyncMigrationMeasurement1.prototype.version); + return this.enqueueStorageOperation(function() { + switch (data) { + case "accepted": + case "declined": + return m.incrementDailyCounter(data); + case "unlinked": + return m.setDailyLastNumeric(data, 1); + default: + Cu.reportError("Unexpected migration field in sync healthreport provider: " + data); + return Promise.resolve(); + } + }); + } + + default: + Cu.reportError("unexpected migration topic in sync healthreport provider: " + topic); + return; + } + }, + + collectDailyData: function () { + return this.storage.enqueueTransaction(this._populateDailyData.bind(this)); + }, + + _populateDailyData: function* () { + let m = this.getMeasurement(SyncMeasurement1.prototype.name, + SyncMeasurement1.prototype.version); + + let svc = Cc["@mozilla.org/weave/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject; + + let enabled = svc.enabled; + yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0); + + // preferredProtocol is constant and only changes as the client + // evolves. + yield m.setDailyLastText("preferredProtocol", "1.5"); + + let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1"; + yield m.setDailyLastText("activeProtocol", protocol); + + if (!enabled) { + return; + } + + // Before grabbing more information, be sure the Sync service + // is fully initialized. This has the potential to initialize + // Sync on the spot. This may be undesired if Sync appears to + // be enabled but it really isn't. That responsibility should + // be up to svc.enabled to not return false positives, however. + yield svc.whenLoaded(); + + if (Weave.Status.service != Weave.STATUS_OK) { + return; + } + + // Device types are dynamic. So we need to dynamically create fields if + // they don't exist. + let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name, + SyncDevicesMeasurement1.prototype.version); + let devices = Weave.Service.clientsEngine.deviceTypes; + for (let [field, count] of devices) { + let hasField = this.storage.hasFieldFromMeasurement(dm.id, field, + this.storage.FIELD_DAILY_LAST_NUMERIC); + let fieldID; + if (hasField) { + fieldID = this.storage.fieldIDFromMeasurement(dm.id, field); + } else { + fieldID = yield this.storage.registerField(dm.id, field, + this.storage.FIELD_DAILY_LAST_NUMERIC); + } + + yield this.storage.setDailyLastNumericFromFieldID(fieldID, count); + } + }, +}); |