Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 #ifndef MERGED_COMPARTMENT
9 this.EXPORTED_SYMBOLS = ["HealthReporter"];
11 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
13 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
15 Cu.import("resource://gre/modules/Metrics.jsm");
16 Cu.import("resource://services-common/async.js");
18 Cu.import("resource://services-common/bagheeraclient.js");
19 #endif
21 Cu.import("resource://gre/modules/Log.jsm");
22 Cu.import("resource://services-common/utils.js");
23 Cu.import("resource://gre/modules/Promise.jsm");
24 Cu.import("resource://gre/modules/osfile.jsm");
25 Cu.import("resource://gre/modules/Preferences.jsm");
26 Cu.import("resource://gre/modules/Services.jsm");
27 Cu.import("resource://gre/modules/Task.jsm");
28 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
29 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
32 "resource://gre/modules/UpdateChannel.jsm");
34 // Oldest year to allow in date preferences. This module was implemented in
35 // 2012 and no dates older than that should be encountered.
36 const OLDEST_ALLOWED_YEAR = 2012;
38 const DAYS_IN_PAYLOAD = 180;
40 const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
42 const TELEMETRY_INIT = "HEALTHREPORT_INIT_MS";
43 const TELEMETRY_INIT_FIRSTRUN = "HEALTHREPORT_INIT_FIRSTRUN_MS";
44 const TELEMETRY_DB_OPEN = "HEALTHREPORT_DB_OPEN_MS";
45 const TELEMETRY_DB_OPEN_FIRSTRUN = "HEALTHREPORT_DB_OPEN_FIRSTRUN_MS";
46 const TELEMETRY_GENERATE_PAYLOAD = "HEALTHREPORT_GENERATE_JSON_PAYLOAD_MS";
47 const TELEMETRY_JSON_PAYLOAD_SERIALIZE = "HEALTHREPORT_JSON_PAYLOAD_SERIALIZE_MS";
48 const TELEMETRY_PAYLOAD_SIZE_UNCOMPRESSED = "HEALTHREPORT_PAYLOAD_UNCOMPRESSED_BYTES";
49 const TELEMETRY_PAYLOAD_SIZE_COMPRESSED = "HEALTHREPORT_PAYLOAD_COMPRESSED_BYTES";
50 const TELEMETRY_UPLOAD = "HEALTHREPORT_UPLOAD_MS";
51 const TELEMETRY_SHUTDOWN_DELAY = "HEALTHREPORT_SHUTDOWN_DELAY_MS";
52 const TELEMETRY_COLLECT_CONSTANT = "HEALTHREPORT_COLLECT_CONSTANT_DATA_MS";
53 const TELEMETRY_COLLECT_DAILY = "HEALTHREPORT_COLLECT_DAILY_MS";
54 const TELEMETRY_SHUTDOWN = "HEALTHREPORT_SHUTDOWN_MS";
55 const TELEMETRY_COLLECT_CHECKPOINT = "HEALTHREPORT_POST_COLLECT_CHECKPOINT_MS";
58 /**
59 * Helper type to assist with management of Health Reporter state.
60 *
61 * Instances are not meant to be created outside of a HealthReporter instance.
62 *
63 * There are two types of IDs associated with clients.
64 *
65 * Since the beginning of FHR, there has existed a per-upload ID: a UUID is
66 * generated at upload time and associated with the state before upload starts.
67 * That same upload includes a request to delete all other upload IDs known by
68 * the client.
69 *
70 * Per-upload IDs had the unintended side-effect of creating "orphaned"
71 * records/upload IDs on the server. So, a stable client identifer has been
72 * introduced. This client identifier is generated when it's missing and sent
73 * as part of every upload.
74 *
75 * There is a high chance we may remove upload IDs in the future.
76 */
77 function HealthReporterState(reporter) {
78 this._reporter = reporter;
80 let profD = OS.Constants.Path.profileDir;
82 if (!profD || !profD.length) {
83 throw new Error("Could not obtain profile directory. OS.File not " +
84 "initialized properly?");
85 }
87 this._log = reporter._log;
89 this._stateDir = OS.Path.join(profD, "healthreport");
91 // To facilitate testing.
92 let leaf = reporter._stateLeaf || "state.json";
94 this._filename = OS.Path.join(this._stateDir, leaf);
95 this._log.debug("Storing state in " + this._filename);
96 this._s = null;
97 }
99 HealthReporterState.prototype = Object.freeze({
100 /**
101 * Persistent string identifier associated with this client.
102 */
103 get clientID() {
104 return this._s.clientID;
105 },
107 /**
108 * The version associated with the client ID.
109 */
110 get clientIDVersion() {
111 return this._s.clientIDVersion;
112 },
114 get lastPingDate() {
115 return new Date(this._s.lastPingTime);
116 },
118 get lastSubmitID() {
119 return this._s.remoteIDs[0];
120 },
122 get remoteIDs() {
123 return this._s.remoteIDs;
124 },
126 get _lastPayloadPath() {
127 return OS.Path.join(this._stateDir, "lastpayload.json");
128 },
130 init: function () {
131 return Task.spawn(function init() {
132 try {
133 OS.File.makeDir(this._stateDir);
134 } catch (ex if ex instanceof OS.FileError) {
135 if (!ex.becauseExists) {
136 throw ex;
137 }
138 }
140 let resetObjectState = function () {
141 this._s = {
142 // The payload version. This is bumped whenever there is a
143 // backwards-incompatible change.
144 v: 1,
145 // The persistent client identifier.
146 clientID: CommonUtils.generateUUID(),
147 // Denotes the mechanism used to generate the client identifier.
148 // 1: Random UUID.
149 clientIDVersion: 1,
150 // Upload IDs that might be on the server.
151 remoteIDs: [],
152 // When we last performed an uploaded.
153 lastPingTime: 0,
154 // Tracks whether we removed an outdated payload.
155 removedOutdatedLastpayload: false,
156 };
157 }.bind(this);
159 try {
160 this._s = yield CommonUtils.readJSON(this._filename);
161 } catch (ex if ex instanceof OS.File.Error) {
162 if (!ex.becauseNoSuchFile) {
163 throw ex;
164 }
166 this._log.warn("Saved state file does not exist.");
167 resetObjectState();
168 } catch (ex) {
169 this._log.error("Exception when reading state from disk: " +
170 CommonUtils.exceptionStr(ex));
171 resetObjectState();
173 // Don't save in case it goes away on next run.
174 }
176 if (typeof(this._s) != "object") {
177 this._log.warn("Read state is not an object. Resetting state.");
178 resetObjectState();
179 yield this.save();
180 }
182 if (this._s.v != 1) {
183 this._log.warn("Unknown version in state file: " + this._s.v);
184 resetObjectState();
185 // We explicitly don't save here in the hopes an application re-upgrade
186 // comes along and fixes us.
187 }
189 let regen = false;
190 if (!this._s.clientID) {
191 this._log.warn("No client ID stored. Generating random ID.");
192 regen = true;
193 }
195 if (typeof(this._s.clientID) != "string") {
196 this._log.warn("Client ID is not a string. Regenerating.");
197 regen = true;
198 }
200 if (regen) {
201 this._s.clientID = CommonUtils.generateUUID();
202 this._s.clientIDVersion = 1;
203 yield this.save();
204 }
206 // Always look for preferences. This ensures that downgrades followed
207 // by reupgrades don't result in excessive data loss.
208 for (let promise of this._migratePrefs()) {
209 yield promise;
210 }
211 }.bind(this));
212 },
214 save: function () {
215 this._log.info("Writing state file: " + this._filename);
216 return CommonUtils.writeJSON(this._s, this._filename);
217 },
219 addRemoteID: function (id) {
220 this._log.warn("Recording new remote ID: " + id);
221 this._s.remoteIDs.push(id);
222 return this.save();
223 },
225 removeRemoteID: function (id) {
226 return this.removeRemoteIDs(id ? [id] : []);
227 },
229 removeRemoteIDs: function (ids) {
230 if (!ids || !ids.length) {
231 this._log.warn("No IDs passed for removal.");
232 return Promise.resolve();
233 }
235 this._log.warn("Removing documents from remote ID list: " + ids);
236 let filtered = this._s.remoteIDs.filter((x) => ids.indexOf(x) === -1);
238 if (filtered.length == this._s.remoteIDs.length) {
239 return Promise.resolve();
240 }
242 this._s.remoteIDs = filtered;
243 return this.save();
244 },
246 setLastPingDate: function (date) {
247 this._s.lastPingTime = date.getTime();
249 return this.save();
250 },
252 updateLastPingAndRemoveRemoteID: function (date, id) {
253 return this.updateLastPingAndRemoveRemoteIDs(date, id ? [id] : []);
254 },
256 updateLastPingAndRemoveRemoteIDs: function (date, ids) {
257 if (!ids) {
258 return this.setLastPingDate(date);
259 }
261 this._log.info("Recording last ping time and deleted remote document.");
262 this._s.lastPingTime = date.getTime();
263 return this.removeRemoteIDs(ids);
264 },
266 /**
267 * Reset the client ID to something else.
268 *
269 * This fails if remote IDs are stored because changing the client ID
270 * while there is remote data will create orphaned records.
271 */
272 resetClientID: function () {
273 if (this.remoteIDs.length) {
274 throw new Error("Cannot reset client ID while remote IDs are stored.");
275 }
277 this._log.warn("Resetting client ID.");
278 this._s.clientID = CommonUtils.generateUUID();
279 this._s.clientIDVersion = 1;
281 return this.save();
282 },
284 _migratePrefs: function () {
285 let prefs = this._reporter._prefs;
287 let lastID = prefs.get("lastSubmitID", null);
288 let lastPingDate = CommonUtils.getDatePref(prefs, "lastPingTime",
289 0, this._log, OLDEST_ALLOWED_YEAR);
291 // If we have state from prefs, migrate and save it to a file then clear
292 // out old prefs.
293 if (lastID || (lastPingDate && lastPingDate.getTime() > 0)) {
294 this._log.warn("Migrating saved state from preferences.");
296 if (lastID) {
297 this._log.info("Migrating last saved ID: " + lastID);
298 this._s.remoteIDs.push(lastID);
299 }
301 let ourLast = this.lastPingDate;
303 if (lastPingDate && lastPingDate.getTime() > ourLast.getTime()) {
304 this._log.info("Migrating last ping time: " + lastPingDate);
305 this._s.lastPingTime = lastPingDate.getTime();
306 }
308 yield this.save();
309 prefs.reset(["lastSubmitID", "lastPingTime"]);
310 } else {
311 this._log.warn("No prefs data found.");
312 }
313 },
314 });
316 /**
317 * This is the abstract base class of `HealthReporter`. It exists so that
318 * we can sanely divide work on platforms where control of Firefox Health
319 * Report is outside of Gecko (e.g., Android).
320 */
321 function AbstractHealthReporter(branch, policy, sessionRecorder) {
322 if (!branch.endsWith(".")) {
323 throw new Error("Branch must end with a period (.): " + branch);
324 }
326 if (!policy) {
327 throw new Error("Must provide policy to HealthReporter constructor.");
328 }
330 this._log = Log.repository.getLogger("Services.HealthReport.HealthReporter");
331 this._log.info("Initializing health reporter instance against " + branch);
333 this._branch = branch;
334 this._prefs = new Preferences(branch);
336 this._policy = policy;
337 this.sessionRecorder = sessionRecorder;
339 this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
341 this._storage = null;
342 this._storageInProgress = false;
343 this._providerManager = null;
344 this._providerManagerInProgress = false;
345 this._initializeStarted = false;
346 this._initialized = false;
347 this._initializeHadError = false;
348 this._initializedDeferred = Promise.defer();
349 this._shutdownRequested = false;
350 this._shutdownInitiated = false;
351 this._shutdownComplete = false;
352 this._shutdownCompleteCallback = null;
354 this._errors = [];
356 this._lastDailyDate = null;
358 // Yes, this will probably run concurrently with remaining constructor work.
359 let hasFirstRun = this._prefs.get("service.firstRun", false);
360 this._initHistogram = hasFirstRun ? TELEMETRY_INIT : TELEMETRY_INIT_FIRSTRUN;
361 this._dbOpenHistogram = hasFirstRun ? TELEMETRY_DB_OPEN : TELEMETRY_DB_OPEN_FIRSTRUN;
362 }
364 AbstractHealthReporter.prototype = Object.freeze({
365 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
367 /**
368 * Whether the service is fully initialized and running.
369 *
370 * If this is false, it is not safe to call most functions.
371 */
372 get initialized() {
373 return this._initialized;
374 },
376 /**
377 * Initialize the instance.
378 *
379 * This must be called once after object construction or the instance is
380 * useless.
381 */
382 init: function () {
383 if (this._initializeStarted) {
384 throw new Error("We have already started initialization.");
385 }
387 this._initializeStarted = true;
389 TelemetryStopwatch.start(this._initHistogram, this);
391 this._initializeState().then(this._onStateInitialized.bind(this),
392 this._onInitError.bind(this));
394 return this.onInit();
395 },
397 //----------------------------------------------------
398 // SERVICE CONTROL FUNCTIONS
399 //
400 // You shouldn't need to call any of these externally.
401 //----------------------------------------------------
403 _onInitError: function (error) {
404 TelemetryStopwatch.cancel(this._initHistogram, this);
405 TelemetryStopwatch.cancel(this._dbOpenHistogram, this);
406 delete this._initHistogram;
407 delete this._dbOpenHistogram;
409 this._recordError("Error during initialization", error);
410 this._initializeHadError = true;
411 this._initiateShutdown();
412 this._initializedDeferred.reject(error);
414 // FUTURE consider poisoning prototype's functions so calls fail with a
415 // useful error message.
416 },
418 _initializeState: function () {
419 return Promise.resolve();
420 },
422 _onStateInitialized: function () {
423 return Task.spawn(function onStateInitialized () {
424 try {
425 if (!this._state._s.removedOutdatedLastpayload) {
426 yield this._deleteOldLastPayload();
427 this._state._s.removedOutdatedLastpayload = true;
428 // Normally we should save this to a file but it directly conflicts with
429 // the "application re-upgrade" decision in HealthReporterState::init()
430 // which specifically does not save the state to a file.
431 }
432 } catch (ex) {
433 this._log.error("Error deleting last payload: " +
434 CommonUtils.exceptionStr(ex));
435 }
436 // As soon as we have could storage, we need to register cleanup or
437 // else bad things happen on shutdown.
438 Services.obs.addObserver(this, "quit-application", false);
439 Services.obs.addObserver(this, "profile-before-change", false);
441 this._storageInProgress = true;
442 TelemetryStopwatch.start(this._dbOpenHistogram, this);
444 Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this),
445 this._onInitError.bind(this));
446 }.bind(this));
447 },
450 /**
451 * Removes the outdated lastpaylaod.json and lastpayload.json.tmp files
452 * @see Bug #867902
453 * @return a promise for when all the files have been deleted
454 */
455 _deleteOldLastPayload: function () {
456 let paths = [this._state._lastPayloadPath, this._state._lastPayloadPath + ".tmp"];
457 return Task.spawn(function removeAllFiles () {
458 for (let path of paths) {
459 try {
460 OS.File.remove(path);
461 } catch (ex) {
462 if (!ex.becauseNoSuchFile) {
463 this._log.error("Exception when removing outdated payload files: " +
464 CommonUtils.exceptionStr(ex));
465 }
466 }
467 }
468 }.bind(this));
469 },
471 // Called when storage has been opened.
472 _onStorageCreated: function (storage) {
473 TelemetryStopwatch.finish(this._dbOpenHistogram, this);
474 delete this._dbOpenHistogram;
475 this._log.info("Storage initialized.");
476 this._storage = storage;
477 this._storageInProgress = false;
479 if (this._shutdownRequested) {
480 this._initiateShutdown();
481 return;
482 }
484 Task.spawn(this._initializeProviderManager.bind(this))
485 .then(this._onProviderManagerInitialized.bind(this),
486 this._onInitError.bind(this));
487 },
489 _initializeProviderManager: function () {
490 if (this._collector) {
491 throw new Error("Provider manager has already been initialized.");
492 }
494 this._log.info("Initializing provider manager.");
495 this._providerManager = new Metrics.ProviderManager(this._storage);
496 this._providerManager.onProviderError = this._recordError.bind(this);
497 this._providerManager.onProviderInit = this._initProvider.bind(this);
498 this._providerManagerInProgress = true;
500 let catString = this._prefs.get("service.providerCategories") || "";
501 if (catString.length) {
502 for (let category of catString.split(",")) {
503 yield this._providerManager.registerProvidersFromCategoryManager(category);
504 }
505 }
506 },
508 _onProviderManagerInitialized: function () {
509 TelemetryStopwatch.finish(this._initHistogram, this);
510 delete this._initHistogram;
511 this._log.debug("Provider manager initialized.");
512 this._providerManagerInProgress = false;
514 if (this._shutdownRequested) {
515 this._initiateShutdown();
516 return;
517 }
519 this._log.info("HealthReporter started.");
520 this._initialized = true;
521 Services.obs.addObserver(this, "idle-daily", false);
523 // If upload is not enabled, ensure daily collection works. If upload
524 // is enabled, this will be performed as part of upload.
525 //
526 // This is important because it ensures about:healthreport contains
527 // longitudinal data even if upload is disabled. Having about:healthreport
528 // provide useful info even if upload is disabled was a core launch
529 // requirement.
530 //
531 // We do not catch changes to the backing pref. So, if the session lasts
532 // many days, we may fail to collect. However, most sessions are short and
533 // this code will likely be refactored as part of splitting up policy to
534 // serve Android. So, meh.
535 if (!this._policy.healthReportUploadEnabled) {
536 this._log.info("Upload not enabled. Scheduling daily collection.");
537 // Since the timer manager is a singleton and there could be multiple
538 // HealthReporter instances, we need to encode a unique identifier in
539 // the timer ID.
540 try {
541 let timerName = this._branch.replace(".", "-", "g") + "lastDailyCollection";
542 let tm = Cc["@mozilla.org/updates/timer-manager;1"]
543 .getService(Ci.nsIUpdateTimerManager);
544 tm.registerTimer(timerName, this.collectMeasurements.bind(this),
545 24 * 60 * 60);
546 } catch (ex) {
547 this._log.error("Error registering collection timer: " +
548 CommonUtils.exceptionStr(ex));
549 }
550 }
552 // Clean up caches and reduce memory usage.
553 this._storage.compact();
554 this._initializedDeferred.resolve(this);
555 },
557 // nsIObserver to handle shutdown.
558 observe: function (subject, topic, data) {
559 switch (topic) {
560 case "quit-application":
561 Services.obs.removeObserver(this, "quit-application");
562 this._initiateShutdown();
563 break;
565 case "profile-before-change":
566 Services.obs.removeObserver(this, "profile-before-change");
567 this._waitForShutdown();
568 break;
570 case "idle-daily":
571 this._performDailyMaintenance();
572 break;
573 }
574 },
576 _initiateShutdown: function () {
577 // Ensure we only begin the main shutdown sequence once.
578 if (this._shutdownInitiated) {
579 this._log.warn("Shutdown has already been initiated. No-op.");
580 return;
581 }
583 this._log.info("Request to shut down.");
585 this._initialized = false;
586 this._shutdownRequested = true;
588 if (this._initializeHadError) {
589 this._log.warn("Initialization had error. Shutting down immediately.");
590 } else {
591 if (this._providerManagerInProcess) {
592 this._log.warn("Provider manager is in progress of initializing. " +
593 "Waiting to finish.");
594 return;
595 }
597 // If storage is in the process of initializing, we need to wait for it
598 // to finish before continuing. The initialization process will call us
599 // again once storage has initialized.
600 if (this._storageInProgress) {
601 this._log.warn("Storage is in progress of initializing. Waiting to finish.");
602 return;
603 }
604 }
606 this._log.warn("Initiating main shutdown procedure.");
608 // Everything from here must only be performed once or else race conditions
609 // could occur.
611 TelemetryStopwatch.start(TELEMETRY_SHUTDOWN, this);
612 this._shutdownInitiated = true;
614 // We may not have registered the observer yet. If not, this will
615 // throw.
616 try {
617 Services.obs.removeObserver(this, "idle-daily");
618 } catch (ex) { }
620 if (this._providerManager) {
621 let onShutdown = this._onProviderManagerShutdown.bind(this);
622 Task.spawn(this._shutdownProviderManager.bind(this))
623 .then(onShutdown, onShutdown);
624 return;
625 }
627 this._log.warn("Don't have provider manager. Proceeding to storage shutdown.");
628 this._shutdownStorage();
629 },
631 _shutdownProviderManager: function () {
632 this._log.info("Shutting down provider manager.");
633 for (let provider of this._providerManager.providers) {
634 try {
635 yield provider.shutdown();
636 } catch (ex) {
637 this._log.warn("Error when shutting down provider: " +
638 CommonUtils.exceptionStr(ex));
639 }
640 }
641 },
643 _onProviderManagerShutdown: function () {
644 this._log.info("Provider manager shut down.");
645 this._providerManager = null;
646 this._shutdownStorage();
647 },
649 _shutdownStorage: function () {
650 if (!this._storage) {
651 this._onShutdownComplete();
652 }
654 this._log.info("Shutting down storage.");
655 let onClose = this._onStorageClose.bind(this);
656 this._storage.close().then(onClose, onClose);
657 },
659 _onStorageClose: function (error) {
660 this._log.info("Storage has been closed.");
662 if (error) {
663 this._log.warn("Error when closing storage: " +
664 CommonUtils.exceptionStr(error));
665 }
667 this._storage = null;
668 this._onShutdownComplete();
669 },
671 _onShutdownComplete: function () {
672 this._log.warn("Shutdown complete.");
673 this._shutdownComplete = true;
674 TelemetryStopwatch.finish(TELEMETRY_SHUTDOWN, this);
676 if (this._shutdownCompleteCallback) {
677 this._shutdownCompleteCallback();
678 }
679 },
681 _waitForShutdown: function () {
682 if (this._shutdownComplete) {
683 return;
684 }
686 TelemetryStopwatch.start(TELEMETRY_SHUTDOWN_DELAY, this);
687 try {
688 this._shutdownCompleteCallback = Async.makeSpinningCallback();
689 this._shutdownCompleteCallback.wait();
690 this._shutdownCompleteCallback = null;
691 } finally {
692 TelemetryStopwatch.finish(TELEMETRY_SHUTDOWN_DELAY, this);
693 }
694 },
696 /**
697 * Convenience method to shut down the instance.
698 *
699 * This should *not* be called outside of tests.
700 */
701 _shutdown: function () {
702 this._initiateShutdown();
703 this._waitForShutdown();
704 },
706 /**
707 * Return a promise that is resolved once the service has been initialized.
708 */
709 onInit: function () {
710 if (this._initializeHadError) {
711 throw new Error("Service failed to initialize.");
712 }
714 if (this._initialized) {
715 return CommonUtils.laterTickResolvingPromise(this);
716 }
718 return this._initializedDeferred.promise;
719 },
721 _performDailyMaintenance: function () {
722 this._log.info("Request to perform daily maintenance.");
724 if (!this._initialized) {
725 return;
726 }
728 let now = new Date();
729 let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1));
731 // The operation is enqueued and put in a transaction by the storage module.
732 this._storage.pruneDataBefore(cutoff);
733 },
735 //--------------------
736 // Provider Management
737 //--------------------
739 /**
740 * Obtain a provider from its name.
741 *
742 * This will only return providers that are currently initialized. If
743 * a provider is lazy initialized (like pull-only providers) this
744 * will likely not return anything.
745 */
746 getProvider: function (name) {
747 if (!this._providerManager) {
748 return null;
749 }
751 return this._providerManager.getProvider(name);
752 },
754 _initProvider: function (provider) {
755 provider.healthReporter = this;
756 },
758 /**
759 * Record an exception for reporting in the payload.
760 *
761 * A side effect is the exception is logged.
762 *
763 * Note that callers need to be extra sensitive about ensuring personal
764 * or otherwise private details do not leak into this. All of the user data
765 * on the stack in FHR code should be limited to data we were collecting with
766 * the intent to submit. So, it is covered under the user's consent to use
767 * the feature.
768 *
769 * @param message
770 * (string) Human readable message describing error.
771 * @param ex
772 * (Error) The error that should be captured.
773 */
774 _recordError: function (message, ex) {
775 let recordMessage = message;
776 let logMessage = message;
778 if (ex) {
779 recordMessage += ": " + CommonUtils.exceptionStr(ex);
780 logMessage += ": " + CommonUtils.exceptionStr(ex);
781 }
783 // Scrub out potentially identifying information from strings that could
784 // make the payload.
785 let appData = Services.dirsvc.get("UAppData", Ci.nsIFile);
786 let profile = Services.dirsvc.get("ProfD", Ci.nsIFile);
788 let appDataURI = Services.io.newFileURI(appData);
789 let profileURI = Services.io.newFileURI(profile);
791 // Order of operation is important here. We do the URI before the path version
792 // because the path may be a subset of the URI. We also have to check for the case
793 // where UAppData is underneath the profile directory (or vice-versa) so we
794 // don't substitute incomplete strings.
796 function replace(uri, path, thing) {
797 // Try is because .spec can throw on invalid URI.
798 try {
799 recordMessage = recordMessage.replace(uri.spec, '<' + thing + 'URI>', 'g');
800 } catch (ex) { }
802 recordMessage = recordMessage.replace(path, '<' + thing + 'Path>', 'g');
803 }
805 if (appData.path.contains(profile.path)) {
806 replace(appDataURI, appData.path, 'AppData');
807 replace(profileURI, profile.path, 'Profile');
808 } else {
809 replace(profileURI, profile.path, 'Profile');
810 replace(appDataURI, appData.path, 'AppData');
811 }
813 this._log.warn(logMessage);
814 this._errors.push(recordMessage);
815 },
817 /**
818 * Collect all measurements for all registered providers.
819 */
820 collectMeasurements: function () {
821 if (!this._initialized) {
822 return Promise.reject(new Error("Not initialized."));
823 }
825 return Task.spawn(function doCollection() {
826 yield this._providerManager.ensurePullOnlyProvidersRegistered();
828 try {
829 TelemetryStopwatch.start(TELEMETRY_COLLECT_CONSTANT, this);
830 yield this._providerManager.collectConstantData();
831 TelemetryStopwatch.finish(TELEMETRY_COLLECT_CONSTANT, this);
832 } catch (ex) {
833 TelemetryStopwatch.cancel(TELEMETRY_COLLECT_CONSTANT, this);
834 this._log.warn("Error collecting constant data: " +
835 CommonUtils.exceptionStr(ex));
836 }
838 // Daily data is collected if it hasn't yet been collected this
839 // application session or if it has been more than a day since the
840 // last collection. This means that providers could see many calls to
841 // collectDailyData per calendar day. However, this collection API
842 // makes no guarantees about limits. The alternative would involve
843 // recording state. The simpler implementation prevails for now.
844 if (!this._lastDailyDate ||
845 Date.now() - this._lastDailyDate > MILLISECONDS_PER_DAY) {
847 try {
848 TelemetryStopwatch.start(TELEMETRY_COLLECT_DAILY, this);
849 this._lastDailyDate = new Date();
850 yield this._providerManager.collectDailyData();
851 TelemetryStopwatch.finish(TELEMETRY_COLLECT_DAILY, this);
852 } catch (ex) {
853 TelemetryStopwatch.cancel(TELEMETRY_COLLECT_DAILY, this);
854 this._log.warn("Error collecting daily data from providers: " +
855 CommonUtils.exceptionStr(ex));
856 }
857 }
859 yield this._providerManager.ensurePullOnlyProvidersUnregistered();
861 // Flush gathered data to disk. This will incur an fsync. But, if
862 // there is ever a time we want to persist data to disk, it's
863 // after a massive collection.
864 try {
865 TelemetryStopwatch.start(TELEMETRY_COLLECT_CHECKPOINT, this);
866 yield this._storage.checkpoint();
867 TelemetryStopwatch.finish(TELEMETRY_COLLECT_CHECKPOINT, this);
868 } catch (ex) {
869 TelemetryStopwatch.cancel(TELEMETRY_COLLECT_CHECKPOINT, this);
870 throw ex;
871 }
873 throw new Task.Result();
874 }.bind(this));
875 },
877 /**
878 * Helper function to perform data collection and obtain the JSON payload.
879 *
880 * If you are looking for an up-to-date snapshot of FHR data that pulls in
881 * new data since the last upload, this is how you should obtain it.
882 *
883 * @param asObject
884 * (bool) Whether to resolve an object or JSON-encoded string of that
885 * object (the default).
886 *
887 * @return Promise<Object | string>
888 */
889 collectAndObtainJSONPayload: function (asObject=false) {
890 if (!this._initialized) {
891 return Promise.reject(new Error("Not initialized."));
892 }
894 return Task.spawn(function collectAndObtain() {
895 yield this._storage.setAutoCheckpoint(0);
896 yield this._providerManager.ensurePullOnlyProvidersRegistered();
898 let payload;
899 let error;
901 try {
902 yield this.collectMeasurements();
903 payload = yield this.getJSONPayload(asObject);
904 } catch (ex) {
905 error = ex;
906 this._collectException("Error collecting and/or retrieving JSON payload",
907 ex);
908 } finally {
909 yield this._providerManager.ensurePullOnlyProvidersUnregistered();
910 yield this._storage.setAutoCheckpoint(1);
912 if (error) {
913 throw error;
914 }
915 }
917 // We hold off throwing to ensure that behavior between finally
918 // and generators and throwing is sane.
919 throw new Task.Result(payload);
920 }.bind(this));
921 },
924 /**
925 * Obtain the JSON payload for currently-collected data.
926 *
927 * The payload only contains data that has been recorded to FHR. Some
928 * providers may have newer data available. If you want to ensure you
929 * have all available data, call `collectAndObtainJSONPayload`
930 * instead.
931 *
932 * @param asObject
933 * (bool) Whether to return an object or JSON encoding of that
934 * object (the default).
935 *
936 * @return Promise<string|object>
937 */
938 getJSONPayload: function (asObject=false) {
939 TelemetryStopwatch.start(TELEMETRY_GENERATE_PAYLOAD, this);
940 let deferred = Promise.defer();
942 Task.spawn(this._getJSONPayload.bind(this, this._now(), asObject)).then(
943 function onResult(result) {
944 TelemetryStopwatch.finish(TELEMETRY_GENERATE_PAYLOAD, this);
945 deferred.resolve(result);
946 }.bind(this),
947 function onError(error) {
948 TelemetryStopwatch.cancel(TELEMETRY_GENERATE_PAYLOAD, this);
949 deferred.reject(error);
950 }.bind(this)
951 );
953 return deferred.promise;
954 },
956 _getJSONPayload: function (now, asObject=false) {
957 let pingDateString = this._formatDate(now);
958 this._log.info("Producing JSON payload for " + pingDateString);
960 // May not be present if we are generating as a result of init error.
961 if (this._providerManager) {
962 yield this._providerManager.ensurePullOnlyProvidersRegistered();
963 }
965 let o = {
966 version: 2,
967 clientID: this._state.clientID,
968 clientIDVersion: this._state.clientIDVersion,
969 thisPingDate: pingDateString,
970 geckoAppInfo: this.obtainAppInfo(this._log),
971 data: {last: {}, days: {}},
972 };
974 let outputDataDays = o.data.days;
976 // Guard here in case we don't track this (e.g., on Android).
977 let lastPingDate = this.lastPingDate;
978 if (lastPingDate && lastPingDate.getTime() > 0) {
979 o.lastPingDate = this._formatDate(lastPingDate);
980 }
982 // We can still generate a payload even if we're not initialized.
983 // This is to facilitate error upload on init failure.
984 if (this._initialized) {
985 for (let provider of this._providerManager.providers) {
986 let providerName = provider.name;
988 let providerEntry = {
989 measurements: {},
990 };
992 // Measurement name to recorded version.
993 let lastVersions = {};
994 // Day string to mapping of measurement name to recorded version.
995 let dayVersions = {};
997 for (let [measurementKey, measurement] of provider.measurements) {
998 let name = providerName + "." + measurement.name;
999 let version = measurement.version;
1001 let serializer;
1002 try {
1003 // The measurement is responsible for returning a serializer which
1004 // is aware of the measurement version.
1005 serializer = measurement.serializer(measurement.SERIALIZE_JSON);
1006 } catch (ex) {
1007 this._recordError("Error obtaining serializer for measurement: " +
1008 name, ex);
1009 continue;
1010 }
1012 let data;
1013 try {
1014 data = yield measurement.getValues();
1015 } catch (ex) {
1016 this._recordError("Error obtaining data for measurement: " + name,
1017 ex);
1018 continue;
1019 }
1021 if (data.singular.size) {
1022 try {
1023 let serialized = serializer.singular(data.singular);
1024 if (serialized) {
1025 // Only replace the existing data if there is no data or if our
1026 // version is newer than the old one.
1027 if (!(name in o.data.last) || version > lastVersions[name]) {
1028 o.data.last[name] = serialized;
1029 lastVersions[name] = version;
1030 }
1031 }
1032 } catch (ex) {
1033 this._recordError("Error serializing singular data: " + name,
1034 ex);
1035 continue;
1036 }
1037 }
1039 let dataDays = data.days;
1040 for (let i = 0; i < DAYS_IN_PAYLOAD; i++) {
1041 let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
1042 if (!dataDays.hasDay(date)) {
1043 continue;
1044 }
1045 let dateFormatted = this._formatDate(date);
1047 try {
1048 let serialized = serializer.daily(dataDays.getDay(date));
1049 if (!serialized) {
1050 continue;
1051 }
1053 if (!(dateFormatted in outputDataDays)) {
1054 outputDataDays[dateFormatted] = {};
1055 }
1057 // This needs to be separate because dayVersions is provider
1058 // specific and gets blown away in a loop while outputDataDays
1059 // is persistent.
1060 if (!(dateFormatted in dayVersions)) {
1061 dayVersions[dateFormatted] = {};
1062 }
1064 if (!(name in outputDataDays[dateFormatted]) ||
1065 version > dayVersions[dateFormatted][name]) {
1066 outputDataDays[dateFormatted][name] = serialized;
1067 dayVersions[dateFormatted][name] = version;
1068 }
1069 } catch (ex) {
1070 this._recordError("Error populating data for day: " + name, ex);
1071 continue;
1072 }
1073 }
1074 }
1075 }
1076 } else {
1077 o.notInitialized = 1;
1078 this._log.warn("Not initialized. Sending report with only error info.");
1079 }
1081 if (this._errors.length) {
1082 o.errors = this._errors.slice(0, 20);
1083 }
1085 if (this._initialized) {
1086 this._storage.compact();
1087 }
1089 if (!asObject) {
1090 TelemetryStopwatch.start(TELEMETRY_JSON_PAYLOAD_SERIALIZE, this);
1091 o = JSON.stringify(o);
1092 TelemetryStopwatch.finish(TELEMETRY_JSON_PAYLOAD_SERIALIZE, this);
1093 }
1095 if (this._providerManager) {
1096 yield this._providerManager.ensurePullOnlyProvidersUnregistered();
1097 }
1099 throw new Task.Result(o);
1100 },
1102 _now: function _now() {
1103 return new Date();
1104 },
1106 // These are stolen from AppInfoProvider.
1107 appInfoVersion: 1,
1108 appInfoFields: {
1109 // From nsIXULAppInfo.
1110 vendor: "vendor",
1111 name: "name",
1112 id: "ID",
1113 version: "version",
1114 appBuildID: "appBuildID",
1115 platformVersion: "platformVersion",
1116 platformBuildID: "platformBuildID",
1118 // From nsIXULRuntime.
1119 os: "OS",
1120 xpcomabi: "XPCOMABI",
1121 },
1123 /**
1124 * Statically return a bundle of app info data, a subset of that produced by
1125 * AppInfoProvider._populateConstants. This allows us to more usefully handle
1126 * payloads that, due to error, contain no data.
1127 *
1128 * Returns a very sparse object if Services.appinfo is unavailable.
1129 */
1130 obtainAppInfo: function () {
1131 let out = {"_v": this.appInfoVersion};
1132 try {
1133 let ai = Services.appinfo;
1134 for (let [k, v] in Iterator(this.appInfoFields)) {
1135 out[k] = ai[v];
1136 }
1137 } catch (ex) {
1138 this._log.warn("Could not obtain Services.appinfo: " +
1139 CommonUtils.exceptionStr(ex));
1140 }
1142 try {
1143 out["updateChannel"] = UpdateChannel.get();
1144 } catch (ex) {
1145 this._log.warn("Could not obtain update channel: " +
1146 CommonUtils.exceptionStr(ex));
1147 }
1149 return out;
1150 },
1151 });
1153 /**
1154 * HealthReporter and its abstract superclass coordinate collection and
1155 * submission of health report metrics.
1156 *
1157 * This is the main type for Firefox Health Report on desktop. It glues all the
1158 * lower-level components (such as collection and submission) together.
1159 *
1160 * An instance of this type is created as an XPCOM service. See
1161 * DataReportingService.js and
1162 * DataReporting.manifest/HealthReportComponents.manifest.
1163 *
1164 * It is theoretically possible to have multiple instances of this running
1165 * in the application. For example, this type may one day handle submission
1166 * of telemetry data as well. However, there is some moderate coupling between
1167 * this type and *the* Firefox Health Report (e.g., the policy). This could
1168 * be abstracted if needed.
1169 *
1170 * Note that `AbstractHealthReporter` exists to allow for Firefox Health Report
1171 * to be more easily implemented on platforms where a separate controlling
1172 * layer is responsible for payload upload and deletion.
1173 *
1174 * IMPLEMENTATION NOTES
1175 * ====================
1176 *
1177 * These notes apply to the combination of `HealthReporter` and
1178 * `AbstractHealthReporter`.
1179 *
1180 * Initialization and shutdown are somewhat complicated and worth explaining
1181 * in extra detail.
1182 *
1183 * The complexity is driven by the requirements of SQLite connection management.
1184 * Once you have a SQLite connection, it isn't enough to just let the
1185 * application shut down. If there is an open connection or if there are
1186 * outstanding SQL statements come XPCOM shutdown time, Storage will assert.
1187 * On debug builds you will crash. On release builds you will get a shutdown
1188 * hang. This must be avoided!
1189 *
1190 * During initialization, the second we create a SQLite connection (via
1191 * Metrics.Storage) we register observers for application shutdown. The
1192 * "quit-application" notification initiates our shutdown procedure. The
1193 * subsequent "profile-do-change" notification ensures it has completed.
1194 *
1195 * The handler for "profile-do-change" may result in event loop spinning. This
1196 * is because of race conditions between our shutdown code and application
1197 * shutdown.
1198 *
1199 * All of our shutdown routines are async. There is the potential that these
1200 * async functions will not complete before XPCOM shutdown. If they don't
1201 * finish in time, we could get assertions in Storage. Our solution is to
1202 * initiate storage early in the shutdown cycle ("quit-application").
1203 * Hopefully all the async operations have completed by the time we reach
1204 * "profile-do-change." If so, great. If not, we spin the event loop until
1205 * they have completed, avoiding potential race conditions.
1206 *
1207 * @param branch
1208 * (string) The preferences branch to use for state storage. The value
1209 * must end with a period (.).
1210 *
1211 * @param policy
1212 * (HealthReportPolicy) Policy driving execution of HealthReporter.
1213 */
1214 this.HealthReporter = function (branch, policy, sessionRecorder, stateLeaf=null) {
1215 this._stateLeaf = stateLeaf;
1216 this._uploadInProgress = false;
1218 AbstractHealthReporter.call(this, branch, policy, sessionRecorder);
1220 if (!this.serverURI) {
1221 throw new Error("No server URI defined. Did you forget to define the pref?");
1222 }
1224 if (!this.serverNamespace) {
1225 throw new Error("No server namespace defined. Did you forget a pref?");
1226 }
1228 this._state = new HealthReporterState(this);
1229 }
1231 this.HealthReporter.prototype = Object.freeze({
1232 __proto__: AbstractHealthReporter.prototype,
1234 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
1236 get lastSubmitID() {
1237 return this._state.lastSubmitID;
1238 },
1240 /**
1241 * When we last successfully submitted data to the server.
1242 *
1243 * This is sent as part of the upload. This is redundant with similar data
1244 * in the policy because we like the modules to be loosely coupled and the
1245 * similar data in the policy is only used for forensic purposes.
1246 */
1247 get lastPingDate() {
1248 return this._state.lastPingDate;
1249 },
1251 /**
1252 * The base URI of the document server to which to submit data.
1253 *
1254 * This is typically a Bagheera server instance. It is the URI up to but not
1255 * including the version prefix. e.g. https://data.metrics.mozilla.com/
1256 */
1257 get serverURI() {
1258 return this._prefs.get("documentServerURI", null);
1259 },
1261 set serverURI(value) {
1262 if (!value) {
1263 throw new Error("serverURI must have a value.");
1264 }
1266 if (typeof(value) != "string") {
1267 throw new Error("serverURI must be a string: " + value);
1268 }
1270 this._prefs.set("documentServerURI", value);
1271 },
1273 /**
1274 * The namespace on the document server to which we will be submitting data.
1275 */
1276 get serverNamespace() {
1277 return this._prefs.get("documentServerNamespace", "metrics");
1278 },
1280 set serverNamespace(value) {
1281 if (!value) {
1282 throw new Error("serverNamespace must have a value.");
1283 }
1285 if (typeof(value) != "string") {
1286 throw new Error("serverNamespace must be a string: " + value);
1287 }
1289 this._prefs.set("documentServerNamespace", value);
1290 },
1292 /**
1293 * Whether this instance will upload data to a server.
1294 */
1295 get willUploadData() {
1296 return this._policy.dataSubmissionPolicyAccepted &&
1297 this._policy.healthReportUploadEnabled;
1298 },
1300 /**
1301 * Whether remote data is currently stored.
1302 *
1303 * @return bool
1304 */
1305 haveRemoteData: function () {
1306 return !!this._state.lastSubmitID;
1307 },
1309 /**
1310 * Called to initiate a data upload.
1311 *
1312 * The passed argument is a `DataSubmissionRequest` from policy.jsm.
1313 */
1314 requestDataUpload: function (request) {
1315 if (!this._initialized) {
1316 return Promise.reject(new Error("Not initialized."));
1317 }
1319 return Task.spawn(function doUpload() {
1320 yield this._providerManager.ensurePullOnlyProvidersRegistered();
1321 try {
1322 yield this.collectMeasurements();
1323 try {
1324 yield this._uploadData(request);
1325 } catch (ex) {
1326 this._onSubmitDataRequestFailure(ex);
1327 }
1328 } finally {
1329 yield this._providerManager.ensurePullOnlyProvidersUnregistered();
1330 }
1331 }.bind(this));
1332 },
1334 /**
1335 * Request that server data be deleted.
1336 *
1337 * If deletion is scheduled to occur immediately, a promise will be returned
1338 * that will be fulfilled when the deletion attempt finishes. Otherwise,
1339 * callers should poll haveRemoteData() to determine when remote data is
1340 * deleted.
1341 */
1342 requestDeleteRemoteData: function (reason) {
1343 if (!this.haveRemoteData()) {
1344 return;
1345 }
1347 return this._policy.deleteRemoteData(reason);
1348 },
1350 _initializeState: function() {
1351 return this._state.init();
1352 },
1354 /**
1355 * Override default handler to incur an upload describing the error.
1356 */
1357 _onInitError: function (error) {
1358 // Need to capture this before we call the parent else it's always
1359 // set.
1360 let inShutdown = this._shutdownRequested;
1362 let result;
1363 try {
1364 result = AbstractHealthReporter.prototype._onInitError.call(this, error);
1365 } catch (ex) {
1366 this._log.error("Error when calling _onInitError: " +
1367 CommonUtils.exceptionStr(ex));
1368 }
1370 // This bypasses a lot of the checks in policy, such as respect for
1371 // backoff. We should arguably not do this. However, reporting
1372 // startup errors is important. And, they should not occur with much
1373 // frequency in the wild. So, it shouldn't be too big of a deal.
1374 if (!inShutdown &&
1375 this._policy.ensureNotifyResponse(new Date()) &&
1376 this._policy.healthReportUploadEnabled) {
1377 // We don't care about what happens to this request. It's best
1378 // effort.
1379 let request = {
1380 onNoDataAvailable: function () {},
1381 onSubmissionSuccess: function () {},
1382 onSubmissionFailureSoft: function () {},
1383 onSubmissionFailureHard: function () {},
1384 onUploadInProgress: function () {},
1385 };
1387 this._uploadData(request);
1388 }
1390 return result;
1391 },
1393 _onBagheeraResult: function (request, isDelete, date, result) {
1394 this._log.debug("Received Bagheera result.");
1396 return Task.spawn(function onBagheeraResult() {
1397 let hrProvider = this.getProvider("org.mozilla.healthreport");
1399 if (!result.transportSuccess) {
1400 // The built-in provider may not be initialized if this instance failed
1401 // to initialize fully.
1402 if (hrProvider && !isDelete) {
1403 hrProvider.recordEvent("uploadTransportFailure", date);
1404 }
1406 request.onSubmissionFailureSoft("Network transport error.");
1407 throw new Task.Result(false);
1408 }
1410 if (!result.serverSuccess) {
1411 if (hrProvider && !isDelete) {
1412 hrProvider.recordEvent("uploadServerFailure", date);
1413 }
1415 request.onSubmissionFailureHard("Server failure.");
1416 throw new Task.Result(false);
1417 }
1419 if (hrProvider && !isDelete) {
1420 hrProvider.recordEvent("uploadSuccess", date);
1421 }
1423 if (isDelete) {
1424 this._log.warn("Marking delete as successful.");
1425 yield this._state.removeRemoteIDs([result.id]);
1426 } else {
1427 this._log.warn("Marking upload as successful.");
1428 yield this._state.updateLastPingAndRemoveRemoteIDs(date, result.deleteIDs);
1429 }
1431 request.onSubmissionSuccess(this._now());
1433 throw new Task.Result(true);
1434 }.bind(this));
1435 },
1437 _onSubmitDataRequestFailure: function (error) {
1438 this._log.error("Error processing request to submit data: " +
1439 CommonUtils.exceptionStr(error));
1440 },
1442 _formatDate: function (date) {
1443 // Why, oh, why doesn't JS have a strftime() equivalent?
1444 return date.toISOString().substr(0, 10);
1445 },
1447 _uploadData: function (request) {
1448 // Under ideal circumstances, clients should never race to this
1449 // function. However, server logs have observed behavior where
1450 // racing to this function could be a cause. So, this lock was
1451 // instituted.
1452 if (this._uploadInProgress) {
1453 this._log.warn("Upload requested but upload already in progress.");
1454 let provider = this.getProvider("org.mozilla.healthreport");
1455 let promise = provider.recordEvent("uploadAlreadyInProgress");
1456 request.onUploadInProgress("Upload already in progress.");
1457 return promise;
1458 }
1460 let id = CommonUtils.generateUUID();
1462 this._log.info("Uploading data to server: " + this.serverURI + " " +
1463 this.serverNamespace + ":" + id);
1464 let client = new BagheeraClient(this.serverURI);
1465 let now = this._now();
1467 return Task.spawn(function doUpload() {
1468 try {
1469 // The test for upload locking monkeypatches getJSONPayload.
1470 // If the next two lines change, be sure to verify the test is
1471 // accurate!
1472 this._uploadInProgress = true;
1473 let payload = yield this.getJSONPayload();
1475 let histogram = Services.telemetry.getHistogramById(TELEMETRY_PAYLOAD_SIZE_UNCOMPRESSED);
1476 histogram.add(payload.length);
1478 let lastID = this.lastSubmitID;
1479 yield this._state.addRemoteID(id);
1481 let hrProvider = this.getProvider("org.mozilla.healthreport");
1482 if (hrProvider) {
1483 let event = lastID ? "continuationUploadAttempt"
1484 : "firstDocumentUploadAttempt";
1485 hrProvider.recordEvent(event, now);
1486 }
1488 TelemetryStopwatch.start(TELEMETRY_UPLOAD, this);
1489 let result;
1490 try {
1491 let options = {
1492 deleteIDs: this._state.remoteIDs.filter((x) => { return x != id; }),
1493 telemetryCompressed: TELEMETRY_PAYLOAD_SIZE_COMPRESSED,
1494 };
1495 result = yield client.uploadJSON(this.serverNamespace, id, payload,
1496 options);
1497 TelemetryStopwatch.finish(TELEMETRY_UPLOAD, this);
1498 } catch (ex) {
1499 TelemetryStopwatch.cancel(TELEMETRY_UPLOAD, this);
1500 if (hrProvider) {
1501 hrProvider.recordEvent("uploadClientFailure", now);
1502 }
1503 throw ex;
1504 }
1506 yield this._onBagheeraResult(request, false, now, result);
1507 } finally {
1508 this._uploadInProgress = false;
1509 }
1510 }.bind(this));
1511 },
1513 /**
1514 * Request deletion of remote data.
1515 *
1516 * @param request
1517 * (DataSubmissionRequest) Tracks progress of this request.
1518 */
1519 deleteRemoteData: function (request) {
1520 if (!this._state.lastSubmitID) {
1521 this._log.info("Received request to delete remote data but no data stored.");
1522 request.onNoDataAvailable();
1523 return;
1524 }
1526 this._log.warn("Deleting remote data.");
1527 let client = new BagheeraClient(this.serverURI);
1529 return Task.spawn(function* doDelete() {
1530 try {
1531 let result = yield client.deleteDocument(this.serverNamespace,
1532 this.lastSubmitID);
1533 yield this._onBagheeraResult(request, true, this._now(), result);
1534 } catch (ex) {
1535 this._log.error("Error processing request to delete data: " +
1536 CommonUtils.exceptionStr(error));
1537 } finally {
1538 // If we don't have any remote documents left, nuke the ID.
1539 // This is done for privacy reasons. Why preserve the ID if we
1540 // don't need to?
1541 if (!this.haveRemoteData()) {
1542 yield this._state.resetClientID();
1543 }
1544 }
1545 }.bind(this));
1546 },
1547 });