michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cu = Components.utils; michael@0: michael@0: const PREF_BRANCH = "toolkit.telemetry."; michael@0: const PREF_ENABLED = PREF_BRANCH + "enabled"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "UITelemetry", michael@0: ]; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm", this); michael@0: michael@0: /** michael@0: * UITelemetry is a helper JSM used to record UI specific telemetry events. michael@0: * michael@0: * It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl. michael@0: */ michael@0: this.UITelemetry = { michael@0: _enabled: undefined, michael@0: _activeSessions: {}, michael@0: _measurements: [], michael@0: michael@0: // Lazily decide whether telemetry is enabled. michael@0: get enabled() { michael@0: if (this._enabled !== undefined) { michael@0: return this._enabled; michael@0: } michael@0: michael@0: // Set an observer to watch for changes at runtime. michael@0: Services.prefs.addObserver(PREF_ENABLED, this, false); michael@0: Services.obs.addObserver(this, "profile-before-change", false); michael@0: michael@0: // Pick up the current value. michael@0: try { michael@0: this._enabled = Services.prefs.getBoolPref(PREF_ENABLED); michael@0: } catch (e) { michael@0: this._enabled = false; michael@0: } michael@0: michael@0: return this._enabled; michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "profile-before-change") { michael@0: Services.obs.removeObserver(this, "profile-before-change"); michael@0: Services.prefs.removeObserver(PREF_ENABLED, this); michael@0: this._enabled = undefined; michael@0: return; michael@0: } michael@0: michael@0: if (aTopic == "nsPref:changed") { michael@0: switch (aData) { michael@0: case PREF_ENABLED: michael@0: let on = Services.prefs.getBoolPref(PREF_ENABLED); michael@0: this._enabled = on; michael@0: michael@0: // Wipe ourselves if we were just disabled. michael@0: if (!on) { michael@0: this._activeSessions = {}; michael@0: this._measurements = []; michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * This exists exclusively for testing -- our events are not intended to michael@0: * be retrieved via an XPCOM interface. michael@0: */ michael@0: get wrappedJSObject() { michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Holds the functions that provide UITelemetry's simple michael@0: * measurements. Those functions are mapped to unique names, michael@0: * and should be registered with addSimpleMeasureFunction. michael@0: */ michael@0: _simpleMeasureFunctions: {}, michael@0: michael@0: /** michael@0: * Adds a single event described by a timestamp, an action, and the calling michael@0: * method. michael@0: * michael@0: * Optionally provide a string 'extras', which will be recorded as part of michael@0: * the event. michael@0: * michael@0: * All extant sessions will be recorded by name for each event. michael@0: */ michael@0: addEvent: function(aAction, aMethod, aTimestamp, aExtras) { michael@0: if (!this.enabled) { michael@0: return; michael@0: } michael@0: michael@0: let sessions = Object.keys(this._activeSessions); michael@0: let aEvent = { michael@0: type: "event", michael@0: action: aAction, michael@0: method: aMethod, michael@0: sessions: sessions, michael@0: timestamp: aTimestamp, michael@0: }; michael@0: michael@0: if (aExtras) { michael@0: aEvent.extras = aExtras; michael@0: } michael@0: michael@0: this._recordEvent(aEvent); michael@0: }, michael@0: michael@0: /** michael@0: * Begins tracking a session by storing a timestamp for session start. michael@0: */ michael@0: startSession: function(aName, aTimestamp) { michael@0: if (!this.enabled) { michael@0: return; michael@0: } michael@0: michael@0: if (this._activeSessions[aName]) { michael@0: // Do not overwrite a previous event start if it already exists. michael@0: return; michael@0: } michael@0: this._activeSessions[aName] = aTimestamp; michael@0: }, michael@0: michael@0: /** michael@0: * Tracks the end of a session with a timestamp. michael@0: */ michael@0: stopSession: function(aName, aReason, aTimestamp) { michael@0: if (!this.enabled) { michael@0: return; michael@0: } michael@0: michael@0: let sessionStart = this._activeSessions[aName]; michael@0: delete this._activeSessions[aName]; michael@0: michael@0: if (!sessionStart) { michael@0: return; michael@0: } michael@0: michael@0: let aEvent = { michael@0: type: "session", michael@0: name: aName, michael@0: reason: aReason, michael@0: start: sessionStart, michael@0: end: aTimestamp, michael@0: }; michael@0: michael@0: this._recordEvent(aEvent); michael@0: }, michael@0: michael@0: _recordEvent: function(aEvent) { michael@0: this._measurements.push(aEvent); michael@0: }, michael@0: michael@0: /** michael@0: * Called by TelemetryPing to populate the simple measurement michael@0: * blob. This function will iterate over all functions added michael@0: * via addSimpleMeasureFunction and return an object with the michael@0: * results of those functions. michael@0: */ michael@0: getSimpleMeasures: function() { michael@0: if (!this.enabled) { michael@0: return {}; michael@0: } michael@0: michael@0: let result = {}; michael@0: for (let name in this._simpleMeasureFunctions) { michael@0: result[name] = this._simpleMeasureFunctions[name](); michael@0: } michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Allows the caller to register functions that will get called michael@0: * for simple measures during a Telemetry ping. aName is a unique michael@0: * identifier used as they key for the simple measurement in the michael@0: * object that getSimpleMeasures returns. michael@0: * michael@0: * This function throws an exception if aName already has a function michael@0: * registered for it. michael@0: */ michael@0: addSimpleMeasureFunction: function(aName, aFunction) { michael@0: if (!this.enabled) { michael@0: return; michael@0: } michael@0: michael@0: if (aName in this._simpleMeasureFunctions) { michael@0: throw new Error("A simple measurement function is already registered for " + aName); michael@0: } michael@0: michael@0: if (!aFunction || typeof aFunction !== 'function') { michael@0: throw new Error("addSimpleMeasureFunction called with non-function argument."); michael@0: } michael@0: michael@0: this._simpleMeasureFunctions[aName] = aFunction; michael@0: }, michael@0: michael@0: removeSimpleMeasureFunction: function(aName) { michael@0: delete this._simpleMeasureFunctions[aName]; michael@0: }, michael@0: michael@0: getUIMeasurements: function() { michael@0: if (!this.enabled) { michael@0: return []; michael@0: } michael@0: michael@0: return this._measurements.slice(); michael@0: } michael@0: };