Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1134 lines
39 KiB
JavaScript
1134 lines
39 KiB
JavaScript
/**
|
|
* This module records app usage data (aggregate time used, number of
|
|
* invocations an uninstalls) for all apps on the phone and periodically
|
|
* transmits that data to Mozilla's telemetry server.
|
|
*
|
|
* Data is only collected and transmitted if the user opts in to telemetry
|
|
* in the FTU or Settings app.
|
|
*
|
|
* A note about time: Date.now() returns absolute time. It changes if the user
|
|
* sets the time in the Settings app, and also can change via the NTP protocol
|
|
* when the device connects to the internet. To avoid having changes in clock
|
|
* time affect our app usage timing data we use performance.now() which
|
|
* returns a relative time whose values do not change when the absolute time
|
|
* changes. However, the batches of metrics we submit do include absolute
|
|
* start and end times. For these times, we do need to use Date.now(), and for
|
|
* values that are compared to the batch start time, we obviously have to
|
|
* use absolute time as well. Note that only absolute time can be persisted
|
|
* since the relative time epoch restarts each time the phone is rebooted.
|
|
*
|
|
* Sometimes on system startup, we see changes to the absolute time of
|
|
* more than a day when we connect to the internet and adjust the time
|
|
* with NTP. These large time changes can skew the batch start times
|
|
* that we report and so this module includes code to adjust the batch
|
|
* start time when the absolute time is changed. This can only be
|
|
* done for fresh batches of metrics that we start ourselves--if we've
|
|
* loaded a persisted batch of metrics, then the start time is from a
|
|
* previous boot of the device and we cannot adjust it. Perhaps when
|
|
* bug 1069863 is fixed we will not have these dramatic time changes when
|
|
* we start up and we can remove the workaround.
|
|
*
|
|
* Known issues:
|
|
*
|
|
* The lockscreen does not generate any events when the user launches the
|
|
* camera from a locked lockscreen, so any time spent using the camera app
|
|
* from the lockscreen will be recorded as time on the lockscreen
|
|
*
|
|
* We would like to be able to record OOMs and similar app failures but
|
|
* the system app window management code does not seem to distinguish
|
|
* normal app termination from abnormal and I can't figure out any way
|
|
* to tell when an app has crashed.
|
|
*/
|
|
|
|
/* global asyncStorage, SettingsListener, performance, SIMSlotManager,
|
|
MobileOperator, uuid, TelemetryRequest, applications,
|
|
LazyLoader */
|
|
(function(exports) {
|
|
'use strict';
|
|
|
|
/*
|
|
* Simple constants used in this module.
|
|
*/
|
|
|
|
// This is the asyncStorage key we use to persist our app usage data so
|
|
// that it survives across device restarts.
|
|
const PERSISTENCE_KEY = 'metrics.app_usage.data.v2';
|
|
|
|
// This is the asyncStorage key we use to persist our device ID
|
|
// v1 of this ID used a randomly generated String, while v2 uses a UUID
|
|
const DEVICE_ID_KEY = 'metrics.app_usage.deviceID.v2';
|
|
|
|
// Various event types we use. Constants here to be sure we use the
|
|
// same values when registering, unregistering and handling these.
|
|
const APPOPENED = 'appopened';
|
|
const HOMESCREEN = 'homescreenopened';
|
|
const ACTIVITY = 'activitycreated';
|
|
const LOCKED = 'lockscreen-appopened'; // In 2.0, use 'locked'
|
|
const UNLOCKED = 'lockscreen-appclosed'; // In 2.0, use 'unlocked'
|
|
const SCREENCHANGE = 'screenchange'; // sleep or wake
|
|
const INSTALL = 'applicationinstall';
|
|
const UNINSTALL = 'applicationuninstall';
|
|
const ONLINE = 'online';
|
|
const OFFLINE = 'offline';
|
|
const TIMECHANGE = 'moztimechange';
|
|
const ATTENTIONOPENED = 'attentionopened';
|
|
const ATTENTIONCLOSED = 'attentionclosed';
|
|
const IDLE = 'idle';
|
|
const ACTIVE = 'active';
|
|
const IACMETRICS = 'iac-app-metrics';
|
|
const ENABLED = 'applicationenabled';
|
|
const DISABLED = 'applicationdisabled';
|
|
|
|
// This is the list of event types we register handlers for
|
|
const EVENT_TYPES = [
|
|
APPOPENED,
|
|
HOMESCREEN,
|
|
ACTIVITY,
|
|
LOCKED,
|
|
UNLOCKED,
|
|
SCREENCHANGE,
|
|
INSTALL,
|
|
UNINSTALL,
|
|
ONLINE,
|
|
OFFLINE,
|
|
TIMECHANGE,
|
|
ATTENTIONOPENED,
|
|
ATTENTIONCLOSED,
|
|
IACMETRICS,
|
|
ENABLED,
|
|
DISABLED
|
|
];
|
|
|
|
|
|
|
|
// This AppUsageMetrics() constructor is the value we export from
|
|
// this module. This constructor does no initialization itself: that
|
|
// is all done in the start() instance method. See bootstrap.js for
|
|
// the code that actually calls the constructor and the start method.
|
|
function AppUsageMetrics() {}
|
|
|
|
// We use the acronym AUM internally to save typing and make
|
|
// references to class variables and constants more legible.
|
|
const AUM = AppUsageMetrics;
|
|
|
|
/*
|
|
* Exported and configurable properties. Properties of AUM are visible
|
|
* outside of the module and are exposed for unit testing and to enable
|
|
* external configuration.
|
|
*/
|
|
|
|
// Export the UsageData class so we can test it independently
|
|
AUM.UsageData = UsageData;
|
|
|
|
// Set to true to to enable debug output
|
|
AUM.DEBUG = false;
|
|
|
|
AUM.MARKETPLACE_ORIGINS = ['https://marketplace.firefox.com'];
|
|
|
|
// This logging function is the only thing that is not exposed through
|
|
// the AppUsageMetrics contstructor or its instance.
|
|
function debug(...args) {
|
|
if (AUM.DEBUG) {
|
|
args.unshift('[AppUsage]');
|
|
console.log.apply(console, args);
|
|
}
|
|
}
|
|
|
|
// What setting do we listen to to turn app usage metrics on or off.
|
|
// This default value is the same setting that turns telemetry on and off.
|
|
AUM.TELEMETRY_ENABLED_KEY = 'metrics.selectedMetrics.level';
|
|
|
|
// For Dogfooders
|
|
AUM.ISDOGFOODER = false;
|
|
|
|
// Base URL for sending data reports
|
|
// Can be overridden with metrics.appusage.reportURL setting.
|
|
AUM.REPORT_URL = 'https://fxos.telemetry.mozilla.org/submit/telemetry';
|
|
|
|
// How often do we try to send the reports
|
|
// Can be overridden with metrics.appusage.reportInterval setting.
|
|
AUM.REPORT_INTERVAL = 14 * 24 * 60 * 60 * 1000; // 2 weeks
|
|
|
|
// If the telemetry server does not respond within this amount of time
|
|
// just give up and try again later.
|
|
// Can be overridden with metrics.appusage.reportTimeout setting.
|
|
AUM.REPORT_TIMEOUT = 20 * 1000; // 20 seconds
|
|
|
|
// If sending a report fails (even though the we're online) how
|
|
// long do we wait before trying it again?
|
|
// Can be overridden with metrics.appusage.retryTimeout setting.
|
|
AUM.RETRY_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
|
|
// How much user idle time (in seconds, not ms) do we wait for before
|
|
// persisting our data to asyncStorage or trying to transmit it.
|
|
AUM.IDLE_TIME = 5; // seconds
|
|
|
|
// Telemetry payload version
|
|
AUM.TELEMETRY_VERSION = 1;
|
|
|
|
// Telemetry "reason" field
|
|
AUM.TELEMETRY_REASON = 'appusage';
|
|
|
|
// App name (static for Telemetry)
|
|
AUM.TELEMETRY_APP_NAME = 'FirefoxOS';
|
|
|
|
|
|
/*
|
|
* AppUsageMetrics instance methods
|
|
*/
|
|
|
|
//
|
|
// The AppUsageMetrics constructor does no initialization of any sort.
|
|
// By system app convention, the initialization code is in this start()
|
|
// instance method instead. Note that this is not the same as the
|
|
// startCollecting() method which is only called if the user has actually
|
|
// opted in to telemetry.
|
|
//
|
|
AUM.prototype.start = function start() {
|
|
LazyLoader.load('shared/js/telemetry.js').then(() => {
|
|
this.reset(); // initialize our state variables
|
|
|
|
// Query and listen for changes on the telemetry setting. Start data
|
|
// collection if or when it becomes set, and stop data collection if
|
|
// it is not set. Note that we do very little initialization here.
|
|
// That happens in the startCollecting() method which is only called if
|
|
// telemetry is actually enabled
|
|
this.metricsEnabledListener = function metricsEnabledListener(enabled) {
|
|
if (enabled === 'Enhanced' || enabled === 'Basic') {
|
|
this.startCollecting();
|
|
}
|
|
else {
|
|
this.stopCollecting();
|
|
}
|
|
}.bind(this);
|
|
|
|
SettingsListener.observe(AUM.TELEMETRY_ENABLED_KEY,
|
|
false, this.metricsEnabledListener);
|
|
});
|
|
};
|
|
|
|
// This method shuts everything down and is only exposed for unit testing.
|
|
// Note that this is not the same as the stopCollecting() method which is
|
|
// used to stop data collection but keep the module running.
|
|
AUM.prototype.stop = function stop() {
|
|
this.stopCollecting();
|
|
SettingsListener.unobserve(AUM.TELEMETRY_ENABLED_KEY,
|
|
this.metricsEnabledListener);
|
|
};
|
|
|
|
// Reset (or initialize) the AppUsageMetrics instance variables
|
|
AUM.prototype.reset = function() {
|
|
// Are we collecting data? This is set to true by startCollecting()
|
|
// and set to false by stopCollecting()
|
|
this.collecting = false;
|
|
|
|
// The UsageData object we're currently collecting data in, or
|
|
// null if we're not collecting anything. Initialized in startCollecting()
|
|
// Note that this object includes a start time for the batch so we know
|
|
// how old it is.
|
|
this.metrics = null;
|
|
|
|
// This is the unique string we send with each batch of data so that
|
|
// batches can be linked together into larger time periods
|
|
this.deviceID = null;
|
|
|
|
// Is the user idle? Updated in handleEvent() based on an idle observer
|
|
this.idle = false;
|
|
|
|
// Is the lockscreen running?
|
|
this.locked = false;
|
|
|
|
// What is the URL of the lockscreen app?
|
|
this.lockscreenApp = null;
|
|
|
|
// A stack of attention window manifest URLs and start times
|
|
this.attentionWindows = [];
|
|
|
|
// When was the last time that a transmission attempt failed.
|
|
// This is used along with the retry interval.
|
|
this.lastFailedTransmission = 0;
|
|
|
|
// This is the URL of the app that the user is currently using. We determine
|
|
// this from appopened events and homescreenopened events. Note that we
|
|
// don't change this variable when the lockscreen is displayed, we just set
|
|
// the locked variable.
|
|
this.currentApp = null;
|
|
|
|
// When did the currently running app start?
|
|
this.currentAppStartTime = performance.now();
|
|
};
|
|
|
|
AUM.prototype.getTopAttentionWindow = function getTopAttentionWindow() {
|
|
return this.attentionWindows ?
|
|
this.attentionWindows[this.attentionWindows.length - 1] :
|
|
undefined;
|
|
};
|
|
|
|
AUM.prototype.getCurrentApp = function() {
|
|
return !this.attentionWindows || this.attentionWindows.length === 0 ?
|
|
this.currentApp : this.getTopAttentionWindow().app;
|
|
};
|
|
|
|
AUM.prototype.getCurrentStartTime = function() {
|
|
return !this.attentionWindows || this.attentionWindows.length === 0 ?
|
|
this.currentAppStartTime : this.getTopAttentionWindow().startTime;
|
|
};
|
|
|
|
AUM.prototype.setCurrentStartTime = function(time) {
|
|
if (!this.attentionWindows || this.attentionWindows.length === 0) {
|
|
this.currentAppStartTime = time;
|
|
} else {
|
|
this.getTopAttentionWindow().startTime = time;
|
|
}
|
|
};
|
|
|
|
// Start collecting app usage data. This function is only called if the
|
|
// appropriate setting is turned on. The done callback is called when
|
|
// setup is complete, but this feature is only needed for tests.
|
|
AUM.prototype.startCollecting = function startCollecting(done) {
|
|
var self = this;
|
|
|
|
debug('starting app usage metrics collection');
|
|
|
|
// If we're already running there is nothing to start
|
|
if (this.collecting) {
|
|
return;
|
|
}
|
|
this.collecting = true;
|
|
|
|
// Begin the startup process by loading data.
|
|
loadData();
|
|
|
|
// Step 1: load any persisted app usage data
|
|
function loadData() {
|
|
// This loads existing data or creates a new object if no data is saved.
|
|
// We store it in the metrics variable that makes it visible throughout
|
|
// the module
|
|
UsageData.load(function(loadedMetrics) {
|
|
// Remember the metrics data
|
|
self.metrics = loadedMetrics;
|
|
|
|
// Now move on to step two in the startup process
|
|
getConfigurationSettings();
|
|
});
|
|
}
|
|
|
|
// Step 2: Configure the server url and other variables by
|
|
// allowing values in the settings database to override the defaults.
|
|
function getConfigurationSettings() {
|
|
// Settings to query, mapped to default values
|
|
var query = {
|
|
'ftu.pingURL': AUM.REPORT_URL,
|
|
'metrics.appusage.reportURL': null,
|
|
'metrics.appusage.reportInterval': AUM.REPORT_INTERVAL,
|
|
'metrics.appusage.reportTimeout': AUM.REPORT_TIMEOUT,
|
|
'metrics.appusage.retryInterval': AUM.RETRY_INTERVAL
|
|
};
|
|
|
|
TelemetryRequest.getSettings(query, function(result) {
|
|
AUM.REPORT_URL = result['metrics.appusage.reportURL'] ||
|
|
result['ftu.pingURL'];
|
|
|
|
AUM.REPORT_INTERVAL = result['metrics.appusage.reportInterval'];
|
|
AUM.REPORT_TIMEOUT = result['metrics.appusage.reportTimeout'];
|
|
AUM.RETRY_INTERVAL = result['metrics.appusage.retryInterval'];
|
|
AUM.ISDOGFOODER = result['debug.performance_data.dogfooding'];
|
|
|
|
// Move on to the next step in the startup process
|
|
getUniqueIdentifier();
|
|
});
|
|
}
|
|
|
|
// Step 3: Look up, or generate a unique identifier for this device
|
|
// so that the periodic metrics reports we send can be linked together
|
|
// to allow analysis over a longer period of time. If the user ever turns
|
|
// off telemetry we will delete this id, so that if it is turned back
|
|
// on, they start off with a clean history
|
|
function getUniqueIdentifier() {
|
|
var promise = TelemetryRequest.getDeviceID(DEVICE_ID_KEY);
|
|
promise.then(function(deviceID) {
|
|
self.deviceID = deviceID;
|
|
waitForApplicationsReady();
|
|
}).catch(function(error) {
|
|
self.deviceID = uuid();
|
|
debug('uuid: ' + self.deviceID);
|
|
asyncStorage.setItem(DEVICE_ID_KEY, self.deviceID);
|
|
waitForApplicationsReady();
|
|
});
|
|
}
|
|
|
|
// Step 4: Ensure the applications cache is ready
|
|
function waitForApplicationsReady() {
|
|
if (applications.ready) {
|
|
registerHandlers();
|
|
return;
|
|
}
|
|
|
|
debug('Waiting for applications to be ready');
|
|
window.addEventListener('applicationready', function onAppsReady(evt) {
|
|
window.removeEventListener('applicationready', onAppsReady);
|
|
registerHandlers();
|
|
});
|
|
}
|
|
|
|
// Step 5: register the various event handlers we need
|
|
function registerHandlers() {
|
|
// Basic event handlers
|
|
EVENT_TYPES.forEach(function(type) {
|
|
window.addEventListener(type, self);
|
|
});
|
|
|
|
self.idleObserver = {
|
|
time: AUM.IDLE_TIME,
|
|
onidle: function() { self.handleEvent(new CustomEvent(IDLE)); },
|
|
onactive: function() { self.handleEvent(new CustomEvent(ACTIVE)); }
|
|
};
|
|
|
|
// Register for idle events
|
|
navigator.addIdleObserver(self.idleObserver);
|
|
|
|
if (done) {
|
|
done();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Stop collecting app usage data and discard any we have already collected.
|
|
// This is called if the setting is turned off.
|
|
AUM.prototype.stopCollecting = function stopCollecting() {
|
|
debug('stopping app usage data collection and deleting stored data');
|
|
|
|
// If we're not already running there is nothing to stop
|
|
if (!this.collecting) {
|
|
return;
|
|
}
|
|
this.collecting = false;
|
|
|
|
// Delete stored data and our device id
|
|
asyncStorage.removeItem(PERSISTENCE_KEY);
|
|
asyncStorage.removeItem(DEVICE_ID_KEY);
|
|
|
|
// Stop listening to all events
|
|
navigator.removeIdleObserver(this.idleObserver);
|
|
this.idleObserver = null;
|
|
|
|
var self = this;
|
|
EVENT_TYPES.forEach(function(type) {
|
|
window.removeEventListener(type, self);
|
|
});
|
|
|
|
// Reset our state, discarding local copies of metrics and deviceID
|
|
this.reset();
|
|
};
|
|
|
|
//
|
|
// This is the heart of this module. It listens to the various events and
|
|
// 1) records app usage data
|
|
// 2) persists app usage data at appropriate times
|
|
// 3) transmits app usage data at appropriate times
|
|
//
|
|
AUM.prototype.handleEvent = function handleEvent(e) {
|
|
var now = performance.now();
|
|
debug('got an event: ', e.type);
|
|
switch (e.type) {
|
|
|
|
case APPOPENED:
|
|
case HOMESCREEN:
|
|
// The user has opened an app, switched apps, or switched to the
|
|
// homescreen. Record data about the app that was running and then
|
|
// update the currently running app.
|
|
this.metrics.recordInvocation(this.getCurrentApp(),
|
|
now - this.getCurrentStartTime());
|
|
this.attentionWindows = [];
|
|
this.currentApp = e.detail;
|
|
this.currentAppStartTime = now;
|
|
break;
|
|
|
|
case ATTENTIONOPENED:
|
|
// Push the current attention screen start time onto stack, and use
|
|
// currentApp / currentAppStartTime when the stack is empty
|
|
this.metrics.recordInvocation(this.getCurrentApp(),
|
|
now - this.getCurrentStartTime());
|
|
this.attentionWindows.push({
|
|
app: e.detail,
|
|
startTime: now
|
|
});
|
|
break;
|
|
|
|
case ACTIVITY:
|
|
// If the current app launches an inline activity we record that
|
|
// and maintain a count of how many times each activity (by url)
|
|
// has been invoked by this app. This will give us interesting data
|
|
// about which apps use which other apps. Note that we do not track
|
|
// the amount of time the user spends in the activity handler.
|
|
this.metrics.recordActivity(this.currentApp, e.detail.url);
|
|
break;
|
|
|
|
case LOCKED:
|
|
// Record the time we ran the app, but keep the app the same
|
|
// because we'll be back to it when the lockscreen is unlocked.
|
|
// Note that if the lockscreen is disabled we won't get this event
|
|
// and will just go straight to the screenchange event. In that
|
|
// case we have to record the invocation when we get that event
|
|
this.metrics.recordInvocation(this.getCurrentApp(),
|
|
now - this.getCurrentStartTime());
|
|
this.setCurrentStartTime(now);
|
|
|
|
// Remember that the lockscreen is active. When we wake up again
|
|
// we need to know this to know whether the user is at the lockscreen
|
|
// or at the current app.
|
|
this.locked = true;
|
|
|
|
// In version 2.1 we use lockscreen-appopened events and get a real URL
|
|
// In 2.0 and before we just use a locked event and don't get the url
|
|
this.lockscreenApp = e.detail;
|
|
break;
|
|
|
|
case UNLOCKED:
|
|
// If the lockscreen was started when the phone went to sleep, then
|
|
// when we wake up we note the time and when we get this event, we
|
|
// record the time spent on the lockscreen.
|
|
if (this.locked && this.lockscreenApp) {
|
|
this.metrics.recordInvocation(this.lockscreenApp,
|
|
now - this.currentAppStartTime);
|
|
|
|
// We left the currentApp unchanged when the phone went to sleep
|
|
// so now that we're leaving the lock screen we will be back at whatever
|
|
// app or homescreen we left. We just have to start timing again
|
|
this.setCurrentStartTime(now);
|
|
}
|
|
this.locked = false;
|
|
break;
|
|
|
|
case SCREENCHANGE:
|
|
if (e.detail.screenOffBy === 'proximity') {
|
|
// Ignore when the screen state changes because of the proximity sensor
|
|
return;
|
|
}
|
|
|
|
if (e.detail.screenEnabled) {
|
|
// We just woke up. Note the time. This will be used for recording
|
|
// time on the lockscreen if we're locked or time at the old app.
|
|
this.setCurrentStartTime(now);
|
|
}
|
|
else {
|
|
// We're going to sleep. If the lockscreen is disabled and we went
|
|
// directly to sleep then record the invocation of the current app.
|
|
// Otherwise, we already recorded that when we got the locked event
|
|
// so now we record lockscreen time. Typically there is just a
|
|
// fraction of a second between the LOCKED and SCREENCHANGE events
|
|
// and the data gets discarded because the time is too short. But
|
|
// if the user wakes the phone up and never unlocks it and then
|
|
// we time out again, we need to record lockscreen time here,
|
|
// not current app time.
|
|
var app = this.locked ? this.lockscreenApp : this.getCurrentApp();
|
|
this.metrics.recordInvocation(app, now - this.getCurrentStartTime());
|
|
}
|
|
break;
|
|
|
|
case ATTENTIONCLOSED:
|
|
// The attention window on top of the stack was closed. When there are
|
|
// other attention windows, we reset the startTime of the top window on
|
|
// the stack. Otherwise we reset the currentApp's start time when the
|
|
// stack is empty.
|
|
var attentionWindow = this.getTopAttentionWindow();
|
|
if (attentionWindow && attentionWindow.app &&
|
|
attentionWindow.app.manifestURL === e.detail.manifestURL) {
|
|
this.metrics.recordInvocation(e.detail,
|
|
now - attentionWindow.startTime);
|
|
this.attentionWindows.pop();
|
|
} else {
|
|
debug('Unexpected attention window closed! ' + e.detail.manifestURL);
|
|
}
|
|
|
|
this.setCurrentStartTime(now);
|
|
break;
|
|
|
|
case INSTALL:
|
|
this.metrics.recordInstall(e.detail.application);
|
|
break;
|
|
|
|
case UNINSTALL:
|
|
this.metrics.recordUninstall(e.detail.application);
|
|
break;
|
|
|
|
case ENABLED:
|
|
this.metrics.recordEnabled(e.detail.application);
|
|
break;
|
|
|
|
case DISABLED:
|
|
this.metrics.recordDisabled(e.detail.application);
|
|
break;
|
|
|
|
case IDLE:
|
|
this.idle = true;
|
|
break;
|
|
|
|
case ACTIVE:
|
|
this.idle = false;
|
|
break;
|
|
|
|
case TIMECHANGE:
|
|
if (this.metrics.relativeStartTime !== undefined) {
|
|
// If we have a relative time recorded for this batch then we
|
|
// can adjust the batch start time on NTP or user time changes.
|
|
// This shouldn't really be necessary but we are seeing some
|
|
// time changes on reboot where the time changes by more than
|
|
// a day when the phone first starts up and connects to a network.
|
|
// This may be caused by bug 1069863, and when that bug is fixed
|
|
// we can consider removing this moztimechange handling code.
|
|
var deltaT = performance.now() - this.metrics.relativeStartTime;
|
|
var oldStartTime = this.metrics.data.start;
|
|
var newStartTime = Date.now() - Math.round(deltaT);
|
|
this.metrics.data.start = newStartTime;
|
|
this.metrics.save(true);
|
|
debug('System time change; converted batch start time from:',
|
|
new Date(oldStartTime).toString(), 'to:',
|
|
new Date(newStartTime).toString());
|
|
}
|
|
break;
|
|
|
|
case IACMETRICS:
|
|
//We need to check this here as we now have a helper module and we
|
|
//don't want to accept any actions we don't handle.
|
|
if (e.detail.action === 'websearch') {
|
|
debug('got a search event for provider: ', e.detail.data);
|
|
this.metrics.recordSearch(e.detail.data);
|
|
}
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* We've updated our state based on the events. Now see whether we should
|
|
* save or transmit the data.
|
|
*/
|
|
|
|
// If we're idle, persist the metrics
|
|
if (this.idle) {
|
|
this.metrics.save(); // Doesn't do anything if metrics have not changed
|
|
}
|
|
|
|
// Is there data to be sent and is this an okay time to send it?
|
|
if (!this.metrics.isEmpty() && this.idle && navigator.onLine) {
|
|
var absoluteTime = Date.now();
|
|
// Have we tried and failed to send it before?
|
|
if (this.lastFailedTransmission > this.metrics.startTime()) {
|
|
|
|
// If so, then send it if the retry interval has elapsed
|
|
if (absoluteTime - this.lastFailedTransmission > AUM.RETRY_INTERVAL) {
|
|
this.transmit();
|
|
}
|
|
}
|
|
// Otherwise, if we have not failed to transmit, then send it if the
|
|
// reporting interval has elapsed.
|
|
else if (absoluteTime - this.metrics.startTime() > AUM.REPORT_INTERVAL) {
|
|
this.transmit();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Transmit the current batch of metrics to the metrics server.
|
|
// Start a new batch of data. If the transmission fails, merge the
|
|
// new batch with the failed batch so we can try again later.
|
|
AUM.prototype.transmit = function transmit() {
|
|
var self = this;
|
|
|
|
if (!this.collecting) {
|
|
return;
|
|
}
|
|
|
|
// Make a private copy of the metrics data we're going to transmit
|
|
var data = JSON.parse(JSON.stringify(this.metrics.data));
|
|
|
|
// Remember the existing metrics in case transmission fails
|
|
var oldMetrics = this.metrics;
|
|
|
|
// But assume that it will succeed and start collecting new metrics now
|
|
this.metrics = new UsageData();
|
|
|
|
// Erase the old data by forcing this new empty batch to be saved.
|
|
// This means that if the phone crashes or is switched off before we
|
|
// transmit the data this batch of data will be lost. That is unlikely
|
|
// to happen and data transmission is optional, so it is not worth
|
|
// the extra effort to design something more robust.
|
|
this.metrics.save(true);
|
|
|
|
// Add some extra data that we want to transmit. These are not things
|
|
// that need to be persisted with the other data, so we just add it now.
|
|
data.stop = Date.now(); // End of batch time; matches data.start
|
|
data.deviceID = this.deviceID; // Link to other batches
|
|
data.locale = navigator.language; // Information about the user's language
|
|
data.screen = { // Information about screen size
|
|
width: screen.width,
|
|
height: screen.height,
|
|
devicePixelRatio: window.devicePixelRatio
|
|
};
|
|
|
|
var deviceInfoQuery = {
|
|
'developer.menu.enabled': false, // If true, data is probably an outlier
|
|
'deviceinfo.hardware': 'unknown',
|
|
'deviceinfo.os': 'unknown',
|
|
'deviceinfo.product_model': 'unknown',
|
|
'deviceinfo.software': 'unknown'
|
|
};
|
|
|
|
var urlInfoQuery = {
|
|
'deviceinfo.platform_build_id': 'unknown',
|
|
'deviceinfo.platform_version': 'unknown',
|
|
'app.update.channel': 'unknown'
|
|
};
|
|
|
|
// Query the settings db to get some more device-specific information
|
|
TelemetryRequest.getSettings(deviceInfoQuery, function(deviceInfo) {
|
|
data.deviceinfo = deviceInfo;
|
|
data.simInfo = getSIMInfo();
|
|
});
|
|
|
|
// Query the settings db for parameters for hte URL
|
|
TelemetryRequest.getSettings(urlInfoQuery, function(urlInfoResponse) {
|
|
// Now transmit the data
|
|
send(data, urlInfoResponse);
|
|
});
|
|
|
|
function getSIMInfo() {
|
|
var simInfo = {
|
|
network: null,
|
|
icc: null
|
|
};
|
|
|
|
if (SIMSlotManager.noSIMCardConnectedToNetwork()) {
|
|
// No connected SIMs
|
|
return simInfo;
|
|
}
|
|
|
|
var slots = SIMSlotManager.getSlots().filter(function(slot) {
|
|
return !slot.isAbsent() && !slot.isLocked();
|
|
});
|
|
|
|
if (slots.length === 0) {
|
|
// No unlocked or active SIM slots
|
|
return simInfo;
|
|
}
|
|
|
|
var conn = slots[0].conn;
|
|
if (!conn) {
|
|
// No connection
|
|
return simInfo;
|
|
}
|
|
|
|
var iccObj = navigator.mozIccManager.getIccById(conn.iccId);
|
|
var iccInfo = iccObj ? iccObj.iccInfo : null;
|
|
var voiceNetwork = conn.voice ? conn.voice.network : null;
|
|
if (!iccInfo && !voiceNetwork) {
|
|
// No voice network or ICC info
|
|
return simInfo;
|
|
}
|
|
|
|
simInfo.network = MobileOperator.userFacingInfo(conn);
|
|
if (voiceNetwork) {
|
|
simInfo.network.mnc = voiceNetwork.mnc;
|
|
simInfo.network.mcc = voiceNetwork.mcc;
|
|
}
|
|
|
|
if (iccInfo) {
|
|
simInfo.icc = {
|
|
mnc: iccInfo.mnc,
|
|
mcc: iccInfo.mcc,
|
|
spn: iccInfo.spn
|
|
};
|
|
}
|
|
|
|
return simInfo;
|
|
}
|
|
|
|
function send(data, urlInfo) {
|
|
var request = new TelemetryRequest({
|
|
reason: AUM.TELEMETRY_REASON,
|
|
deviceID: self.deviceID,
|
|
ver: AUM.TELEMETRY_VERSION,
|
|
url: AUM.REPORT_URL,
|
|
appUpdateChannel: urlInfo['app.update.channel'],
|
|
appVersion: urlInfo['deviceinfo.platform_version'],
|
|
appBuildID: urlInfo['deviceinfo.platform_build_id']
|
|
}, data);
|
|
|
|
// We don't actually have to do anything if the data is transmitted
|
|
// successfully.
|
|
// We are already set up to collect the next batch of data.
|
|
function onload() {
|
|
debug('Transmitted app usage data to', request.url);
|
|
}
|
|
|
|
function retry(e) {
|
|
// If the attempt to transmit a batch of data fails, we'll merge
|
|
// the new batch of data (which may be empty) in with the old one
|
|
// and resave everything so we can try again later. We also record
|
|
// the time of this failure so we don't try sending again too soon.
|
|
debug('App usage metrics transmission failure:', e.type);
|
|
|
|
// We use absolute time here because we will be comparing to
|
|
// the absolute batch start time.
|
|
self.lastFailedTransmission = Date.now();
|
|
oldMetrics.merge(self.metrics);
|
|
self.metrics = oldMetrics;
|
|
self.metrics.save(true);
|
|
}
|
|
|
|
request.send({
|
|
timeout: AUM.REPORT_TIMEOUT,
|
|
onload: onload,
|
|
onerror: retry,
|
|
onabort: retry,
|
|
ontimeout: retry
|
|
});
|
|
}
|
|
};
|
|
|
|
/*
|
|
* A helper class that holds (and persists) a batch of app usage data
|
|
*/
|
|
function UsageData() {
|
|
this.data = {
|
|
start: Date.now(),
|
|
apps: {}, // Maps app URLs to usage data
|
|
searches: {}
|
|
};
|
|
this.needsSave = false;
|
|
// Record the relative start time, which we can use to adjust
|
|
// this.data.start if we get a moztimechange event.
|
|
this.relativeStartTime = performance.now();
|
|
}
|
|
|
|
/*
|
|
* Get app usage for the current date
|
|
*/
|
|
UsageData.prototype.getAppUsage = function(app, dayKey) {
|
|
var manifestURL;
|
|
var addOn = false;
|
|
|
|
if (app !== null && typeof app === 'object') {
|
|
manifestURL = app.manifestURL;
|
|
addOn = (app.manifest.role === 'addon');
|
|
} else {
|
|
manifestURL = app;
|
|
}
|
|
|
|
var usage = this.data.apps[manifestURL];
|
|
dayKey = dayKey || this.getDayKey();
|
|
|
|
// We lazily initialize both the per-app and per-day usage maps
|
|
if (!usage) {
|
|
this.data.apps[manifestURL] = usage = {};
|
|
}
|
|
|
|
var dayUsage = usage[dayKey];
|
|
if (!dayUsage) {
|
|
dayUsage = usage[dayKey] = {
|
|
usageTime: 0,
|
|
invocations: 0,
|
|
installs: 0,
|
|
uninstalls: 0,
|
|
enables: 0,
|
|
disables: 0,
|
|
activities: {},
|
|
addOn: addOn
|
|
};
|
|
this.data.apps[manifestURL] = usage;
|
|
}
|
|
|
|
// Set 'add-on' attribute here so that existing usage metrics
|
|
// (usage metrics recorded before we started identifying add-ons
|
|
// -- bug 1198346) get the 'add-on' attribute.
|
|
dayUsage.addOn = addOn;
|
|
return dayUsage;
|
|
};
|
|
|
|
UsageData.prototype.getDayKey = function(date) {
|
|
date = date || new Date();
|
|
var dayKey = date.toISOString().substring(0, 10);
|
|
return dayKey.replace(/-/g, '');
|
|
};
|
|
|
|
UsageData.prototype.getSearchCounts = function(provider) {
|
|
var search = this.data.searches[provider];
|
|
var dayKey = this.getDayKey();
|
|
if (!search) {
|
|
// If no usage exists for this provider, create a new empty object for it.
|
|
this.data.searches[provider] = search = {};
|
|
debug('creating new object for provider', provider);
|
|
}
|
|
|
|
var daySearch = search[dayKey];
|
|
if (!daySearch) {
|
|
daySearch = search[dayKey] = {
|
|
count: 0
|
|
};
|
|
}
|
|
return daySearch;
|
|
};
|
|
|
|
UsageData.prototype.startTime = function() {
|
|
return this.data.start;
|
|
};
|
|
|
|
UsageData.prototype.isEmpty = function() {
|
|
return Object.keys(this.data.apps).length === 0;
|
|
};
|
|
|
|
// We only care about recording certain kinds of apps:
|
|
// - Apps pre-installed with the phone (certified, or using a gaia origin)
|
|
// - Apps installed from the marketplace
|
|
UsageData.prototype.shouldTrackApp = function(app) {
|
|
if (!app) {
|
|
return false;
|
|
}
|
|
|
|
// Bug 1134998: Don't track apps that are marked as private windows
|
|
// Some app-like objects may not have the isPrivateBrowser function,
|
|
// so we also check to make sure it exists here.
|
|
if (typeof app.isPrivateBrowser === 'function' && app.isPrivateBrowser()) {
|
|
return false;
|
|
}
|
|
|
|
// Gecko and the app window state machine do not send certain app properties
|
|
// along in webapp-launch or appopened events, causing marketplace app usage
|
|
// to not be properly recorded. We fall back on the system app's application
|
|
// cache in these situations. See Bug 1137063
|
|
var cachedApp = applications.getByManifestURL(app.manifestURL);
|
|
var manifest = app.manifest || app.updateManifest;
|
|
if (!manifest && cachedApp) {
|
|
manifest = cachedApp.manifest || cachedApp.updateManifest;
|
|
}
|
|
|
|
var installOrigin = app.installOrigin;
|
|
if (!installOrigin && cachedApp) {
|
|
installOrigin = cachedApp.installOrigin;
|
|
}
|
|
|
|
var type = manifest ? manifest.type : 'unknown';
|
|
if (type === 'certified') {
|
|
return true;
|
|
}
|
|
|
|
debug('shouldTrackApp: MARKETPLACE_ORIGINS: ',
|
|
AUM.MARKETPLACE_ORIGINS);
|
|
if (AUM.MARKETPLACE_ORIGINS.indexOf(installOrigin) >= 0) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
var url = new URL(app.manifestURL);
|
|
return url.hostname.indexOf('gaiamobile.org') >= 0;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
UsageData.prototype.recordInvocation = function(app, time) {
|
|
if (!this.shouldTrackApp(app)) {
|
|
return false;
|
|
}
|
|
|
|
// Convert time to seconds and round to the nearest second. If 0,
|
|
// don't record anything. (This can happen when we go to the
|
|
// lockscreen right before sleeping, for example.)
|
|
time = Math.round(time / 1000);
|
|
if (time > 0) {
|
|
var usage = this.getAppUsage(app);
|
|
usage.invocations++;
|
|
usage.usageTime += time;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'ran for', time);
|
|
}
|
|
return time > 0;
|
|
};
|
|
|
|
UsageData.prototype.recordSearch = function(provider) {
|
|
debug('recordSearch', provider);
|
|
|
|
if (provider == null) {
|
|
return;
|
|
}
|
|
|
|
// We don't want to report search metrics for local search and any other
|
|
// situation where we might be offline. Check this here as this may change
|
|
// in the future.
|
|
if (navigator.onLine) {
|
|
var search = this.getSearchCounts(provider);
|
|
search.count++;
|
|
debug('Search Count for: ' + provider + ': ', search.count);
|
|
this.needsSave = true;
|
|
}
|
|
};
|
|
|
|
UsageData.prototype.recordInstall = function(app) {
|
|
var self = this;
|
|
debug('recordInstall for app:', app);
|
|
if (!this.shouldTrackApp(app)) {
|
|
debug('recordInstall, skip this one');
|
|
return false;
|
|
}
|
|
|
|
// If the app manifest is undefined, this means we need to listen to the
|
|
// downloadsuccess event fired when the download is finished. The full
|
|
// manifest will be available on the downloadsuccess.
|
|
if (typeof app.manifest === 'undefined') {
|
|
app.addEventListener('downloadsuccess', function onSuccessfulDownload() {
|
|
app.removeEventListener('downloadsuccess', onSuccessfulDownload);
|
|
self.incrementInstallRecord(app);
|
|
});
|
|
return false;
|
|
} else {
|
|
self.incrementInstallRecord(app);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
UsageData.prototype.incrementInstallRecord = function(app) {
|
|
var usage = this.getAppUsage(app);
|
|
usage.installs++;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'installed');
|
|
};
|
|
|
|
UsageData.prototype.recordUninstall = function(app) {
|
|
if (!this.shouldTrackApp(app)) {
|
|
return false;
|
|
}
|
|
|
|
var usage = this.getAppUsage(app);
|
|
usage.uninstalls++;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'uninstalled');
|
|
return true;
|
|
};
|
|
|
|
UsageData.prototype.recordEnabled = function(app) {
|
|
if (!this.shouldTrackApp(app)) {
|
|
return false;
|
|
}
|
|
|
|
var usage = this.getAppUsage(app);
|
|
usage.enables++;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'enabled');
|
|
return true;
|
|
};
|
|
|
|
UsageData.prototype.recordDisabled = function(app) {
|
|
if (!this.shouldTrackApp(app)) {
|
|
return false;
|
|
}
|
|
|
|
var usage = this.getAppUsage(app);
|
|
usage.disables++;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'disabled');
|
|
return true;
|
|
};
|
|
|
|
UsageData.prototype.recordActivity = function(app, url) {
|
|
if (!this.shouldTrackApp(app)) {
|
|
return false;
|
|
}
|
|
|
|
var usage = this.getAppUsage(app);
|
|
var count = usage.activities[url] || 0;
|
|
usage.activities[url] = ++count;
|
|
this.needsSave = true;
|
|
debug(app.manifestURL, 'invoked activity', url);
|
|
return true;
|
|
};
|
|
|
|
// Merge a newer batch of data into this older batch.
|
|
// We use this to recover from metrics transmission failures
|
|
UsageData.prototype.merge = function(newbatch) {
|
|
// Since we transmit while the user is idle, often there will not be
|
|
// any new data collected while we're trying to transmit and in that
|
|
// case there is nothing to merge.
|
|
if (!newbatch || newbatch.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Otherwise, loop through all the apps that we have data for
|
|
// in the new batch and merge the new usage data with the old
|
|
// usage data.
|
|
for (var app in newbatch.data.apps) {
|
|
var newdays = newbatch.data.apps[app];
|
|
for (var day in newdays) {
|
|
var newusage = newdays[day];
|
|
var oldusage = this.getAppUsage(app, day);
|
|
|
|
oldusage.usageTime += newusage.usageTime;
|
|
oldusage.invocations += newusage.invocations;
|
|
oldusage.installs += newusage.installs;
|
|
oldusage.uninstalls += newusage.uninstalls;
|
|
oldusage.enables += newusage.enables;
|
|
oldusage.disables += newusage.disables;
|
|
|
|
for (var url in newusage.activities) {
|
|
var newcount = newusage.activities[url];
|
|
var oldcount = oldusage.activities[url] || 0;
|
|
oldusage.activities[url] = oldcount + newcount;
|
|
}
|
|
}
|
|
}
|
|
|
|
// loop through all the search providers that we have data for
|
|
// and merge the new searches into the old searches.
|
|
for (var provider in newbatch.data.searches) {
|
|
var newsearch = newbatch.data.searches[provider];
|
|
var oldsearch = this.data.searches[provider];
|
|
|
|
if (!oldsearch) {
|
|
// If no usage exists for this provider, create a new empty object.
|
|
this.data.searches[provider] = {};
|
|
debug('creating new object for provider', provider);
|
|
}
|
|
|
|
for (var daykey in newsearch) {
|
|
var daySearch = oldsearch[daykey];
|
|
if (!daySearch) {
|
|
oldsearch[daykey] = newsearch[daykey];
|
|
} else {
|
|
var newsearchcount = newsearch[daykey].count;
|
|
var oldsearchcount = oldsearch[daykey].count || 0;
|
|
oldsearch[daykey].count = oldsearchcount + newsearchcount;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Persist the current batch of metrics so we don't lose it if the user
|
|
// switches the phone off.
|
|
UsageData.prototype.save = function(force) {
|
|
if (force || this.needsSave) {
|
|
asyncStorage.setItem(PERSISTENCE_KEY, this.data);
|
|
this.needsSave = false;
|
|
debug('Saved app usage data');
|
|
}
|
|
};
|
|
|
|
// Load the current metrics from persistant storage.
|
|
// Note that this is an async factory method, not an instance method.
|
|
UsageData.load = function(callback) {
|
|
asyncStorage.getItem(PERSISTENCE_KEY, function(data) {
|
|
var usage = new UsageData();
|
|
if (data) {
|
|
usage.data = data;
|
|
//Handle a scenario with old app data that does not have searches
|
|
if (typeof usage.data.searches === 'undefined') {
|
|
usage.data.searches = {};
|
|
}
|
|
|
|
// If we loaded persisted data, then the absolute start time can
|
|
// and should no longer be adjusted. So remove the relative time.
|
|
delete usage.relativeStartTime;
|
|
}
|
|
callback(usage);
|
|
});
|
|
};
|
|
|
|
// The AppUsageMetrics constructor is the single value we export.
|
|
exports.AppUsageMetrics = AppUsageMetrics;
|
|
}(window));
|