Merge pull request #163 from mozilla/testpilot-metrics-117
for #117: start telemetry with testpilot-metrics
This commit is contained in:
commit
0e015337bb
6 changed files with 481 additions and 8 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
testpilot-metrics.js
|
|
@ -114,7 +114,6 @@ of a `testpilottest` telemetry ping for each scenario.
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"uuid": <uuid>,
|
"uuid": <uuid>,
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "add-container"
|
"event": "add-container"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
146
index.js
146
index.js
|
@ -35,6 +35,7 @@ const PREFS = [
|
||||||
const { attachTo, detachFrom } = require("sdk/content/mod");
|
const { attachTo, detachFrom } = require("sdk/content/mod");
|
||||||
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
|
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
|
||||||
const { getFavicon } = require("sdk/places/favicon");
|
const { getFavicon } = require("sdk/places/favicon");
|
||||||
|
const Metrics = require("./testpilot-metrics");
|
||||||
const { modelFor } = require("sdk/model/core");
|
const { modelFor } = require("sdk/model/core");
|
||||||
const prefService = require("sdk/preferences/service");
|
const prefService = require("sdk/preferences/service");
|
||||||
const self = require("sdk/self");
|
const self = require("sdk/self");
|
||||||
|
@ -42,11 +43,13 @@ const ss = require("sdk/simple-storage");
|
||||||
const { Style } = require("sdk/stylesheet/style");
|
const { Style } = require("sdk/stylesheet/style");
|
||||||
const tabs = require("sdk/tabs");
|
const tabs = require("sdk/tabs");
|
||||||
const tabsUtils = require("sdk/tabs/utils");
|
const tabsUtils = require("sdk/tabs/utils");
|
||||||
|
const uuid = require("sdk/util/uuid");
|
||||||
const { viewFor } = require("sdk/view/core");
|
const { viewFor } = require("sdk/view/core");
|
||||||
const webExtension = require("sdk/webextension");
|
const webExtension = require("sdk/webextension");
|
||||||
const windows = require("sdk/windows");
|
const windows = require("sdk/windows");
|
||||||
const windowUtils = require("sdk/window/utils");
|
const windowUtils = require("sdk/window/utils");
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// ContainerService
|
// ContainerService
|
||||||
|
|
||||||
|
@ -61,7 +64,8 @@ const ContainerService = {
|
||||||
if (installation) {
|
if (installation) {
|
||||||
const object = {
|
const object = {
|
||||||
version: 1,
|
version: 1,
|
||||||
prefs: {}
|
prefs: {},
|
||||||
|
metricsUUID: uuid.uuid().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
PREFS.forEach(pref => {
|
PREFS.forEach(pref => {
|
||||||
|
@ -77,6 +81,8 @@ const ContainerService = {
|
||||||
prefService.set(pref[0], pref[1]);
|
prefService.set(pref[0], pref[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._metricsUUID = ss.storage.savedConfiguration.metricsUUID;
|
||||||
|
|
||||||
// Message routing
|
// Message routing
|
||||||
|
|
||||||
// only these methods are allowed. We have a 1:1 mapping between messages
|
// only these methods are allowed. We have a 1:1 mapping between messages
|
||||||
|
@ -95,6 +101,7 @@ const ContainerService = {
|
||||||
"removeIdentity",
|
"removeIdentity",
|
||||||
"updateIdentity",
|
"updateIdentity",
|
||||||
"getPreference",
|
"getPreference",
|
||||||
|
"sendTelemetryPayload"
|
||||||
];
|
];
|
||||||
|
|
||||||
// Map of identities.
|
// Map of identities.
|
||||||
|
@ -159,10 +166,65 @@ const ContainerService = {
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
throw new Error("WebExtension startup failed. Unable to continue.");
|
throw new Error("WebExtension startup failed. Unable to continue.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._sendEvent = new Metrics({
|
||||||
|
type: "sdk",
|
||||||
|
id: self.id,
|
||||||
|
version: self.version
|
||||||
|
}).sendEvent;
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// utility methods
|
// utility methods
|
||||||
|
|
||||||
|
_containerTabCount(userContextId) {
|
||||||
|
// Returns the total of open and hidden tabs with this userContextId
|
||||||
|
let containerTabsCount = 0;
|
||||||
|
containerTabsCount += this._identitiesState[userContextId].openTabs;
|
||||||
|
containerTabsCount += this._identitiesState[userContextId].hiddenTabs.length;
|
||||||
|
return containerTabsCount;
|
||||||
|
},
|
||||||
|
|
||||||
|
_totalContainerTabsCount() {
|
||||||
|
// Returns the number of total open tabs across ALL containers
|
||||||
|
let totalContainerTabsCount = 0;
|
||||||
|
for (const userContextId in this._identitiesState) {
|
||||||
|
totalContainerTabsCount += this._identitiesState[userContextId].openTabs;
|
||||||
|
}
|
||||||
|
return totalContainerTabsCount;
|
||||||
|
},
|
||||||
|
|
||||||
|
_totalNonContainerTabsCount() {
|
||||||
|
// Returns the number of open tabs NOT IN a container
|
||||||
|
let totalNonContainerTabsCount = 0;
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (this._getUserContextIdFromTab(tab) === 0) {
|
||||||
|
++totalNonContainerTabsCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalNonContainerTabsCount;
|
||||||
|
},
|
||||||
|
|
||||||
|
_containersCounts() {
|
||||||
|
let containersCounts = { // eslint-disable-line prefer-const
|
||||||
|
"shown": 0,
|
||||||
|
"hidden": 0,
|
||||||
|
"total": 0
|
||||||
|
};
|
||||||
|
for (const userContextId in this._identitiesState) {
|
||||||
|
if (this._identitiesState[userContextId].openTabs > 0) {
|
||||||
|
++containersCounts.shown;
|
||||||
|
++containersCounts.total;
|
||||||
|
continue;
|
||||||
|
} else if (this._identitiesState[userContextId].hiddenTabs.length > 0) {
|
||||||
|
++containersCounts.hidden;
|
||||||
|
++containersCounts.total;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return containersCounts;
|
||||||
|
},
|
||||||
|
|
||||||
_convert(identity) {
|
_convert(identity) {
|
||||||
// Let's convert the known colors to their color names.
|
// Let's convert the known colors to their color names.
|
||||||
return {
|
return {
|
||||||
|
@ -291,6 +353,17 @@ const ContainerService = {
|
||||||
ss.storage.identitiesData = this._identitiesState;
|
ss.storage.identitiesData = this._identitiesState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sendTelemetryPayload(args = {}) {
|
||||||
|
// when pings come from popup, delete "method" prop
|
||||||
|
delete args.method;
|
||||||
|
let payload = { // eslint-disable-line prefer-const
|
||||||
|
"uuid": this._metricsUUID
|
||||||
|
};
|
||||||
|
Object.assign(payload, args);
|
||||||
|
|
||||||
|
this._sendEvent(payload);
|
||||||
|
},
|
||||||
|
|
||||||
// Tabs management
|
// Tabs management
|
||||||
|
|
||||||
hideTabs(args) {
|
hideTabs(args) {
|
||||||
|
@ -303,6 +376,16 @@ const ContainerService = {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containersCounts = this._containersCounts();
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "hide-tabs",
|
||||||
|
"userContextId": args.userContextId,
|
||||||
|
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
||||||
|
"shownContainersCount": containersCounts.shown,
|
||||||
|
"hiddenContainersCount": containersCounts.hidden,
|
||||||
|
"totalContainersCount": containersCounts.total
|
||||||
|
});
|
||||||
|
|
||||||
const tabsToClose = [];
|
const tabsToClose = [];
|
||||||
|
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
this._containerTabIterator(args.userContextId, tab => {
|
||||||
|
@ -337,6 +420,16 @@ const ContainerService = {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containersCounts = this._containersCounts();
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "show-tabs",
|
||||||
|
"userContextId": args.userContextId,
|
||||||
|
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
||||||
|
"shownContainersCount": containersCounts.shown,
|
||||||
|
"hiddenContainersCount": containersCounts.hidden,
|
||||||
|
"totalContainersCount": containersCounts.total
|
||||||
|
});
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
for (let object of this._identitiesState[args.userContextId].hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of this._identitiesState[args.userContextId].hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
|
@ -351,6 +444,13 @@ const ContainerService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
sortTabs() {
|
sortTabs() {
|
||||||
|
const containersCounts = this._containersCounts();
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "sort-tabs",
|
||||||
|
"shownContainersCount": containersCounts.shown,
|
||||||
|
"totalContainerTabsCount": this._totalContainerTabsCount(),
|
||||||
|
"totalNonContainerTabsCount": this._totalNonContainerTabsCount()
|
||||||
|
});
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
||||||
// First the pinned tabs, then the normal ones.
|
// First the pinned tabs, then the normal ones.
|
||||||
|
@ -460,6 +560,12 @@ const ContainerService = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "move-tabs-to-window",
|
||||||
|
"userContextId": args.userContextId,
|
||||||
|
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
||||||
|
});
|
||||||
|
|
||||||
// Let's create a list of the tabs.
|
// Let's create a list of the tabs.
|
||||||
const list = [];
|
const list = [];
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
this._containerTabIterator(args.userContextId, tab => {
|
||||||
|
@ -500,9 +606,17 @@ const ContainerService = {
|
||||||
|
|
||||||
openTab(args) {
|
openTab(args) {
|
||||||
return this._recentBrowserWindow().then(browserWin => {
|
return this._recentBrowserWindow().then(browserWin => {
|
||||||
let userContextId = 0;
|
const userContextId = ("userContextId" in args) ? args.userContextId : 0;
|
||||||
if ("userContextId" in args) {
|
const source = ("source" in args) ? args.source : null;
|
||||||
userContextId = args.userContextId;
|
|
||||||
|
// Only send telemetry for tabs opened by UI - i.e., not via showTabs
|
||||||
|
if (source) {
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "open-tab",
|
||||||
|
"eventSource": source,
|
||||||
|
"userContextId": userContextId,
|
||||||
|
"clickedContainerTabCount": this._containerTabCount(userContextId)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tab = browserWin.gBrowser.addTab(args.url || DEFAULT_TAB, { userContextId });
|
const tab = browserWin.gBrowser.addTab(args.url || DEFAULT_TAB, { userContextId });
|
||||||
|
@ -538,6 +652,10 @@ const ContainerService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
createIdentity(args) {
|
createIdentity(args) {
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "add-container",
|
||||||
|
});
|
||||||
|
|
||||||
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
||||||
if (!(arg in args)) {
|
if (!(arg in args)) {
|
||||||
return Promise.reject("createIdentity must be called with " + arg + " argument.");
|
return Promise.reject("createIdentity must be called with " + arg + " argument.");
|
||||||
|
@ -563,6 +681,11 @@ const ContainerService = {
|
||||||
return Promise.reject("updateIdentity must be called with userContextId argument.");
|
return Promise.reject("updateIdentity must be called with userContextId argument.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "edit-container",
|
||||||
|
"userContextId": args.userContextId
|
||||||
|
});
|
||||||
|
|
||||||
const identity = ContextualIdentityService.getIdentityFromId(args.userContextId);
|
const identity = ContextualIdentityService.getIdentityFromId(args.userContextId);
|
||||||
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
||||||
if ((arg in args)) {
|
if ((arg in args)) {
|
||||||
|
@ -589,6 +712,11 @@ const ContainerService = {
|
||||||
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "delete-container",
|
||||||
|
"userContextId": args.userContextId
|
||||||
|
});
|
||||||
|
|
||||||
const tabsToClose = [];
|
const tabsToClose = [];
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
this._containerTabIterator(args.userContextId, tab => {
|
||||||
tabsToClose.push(tab);
|
tabsToClose.push(tab);
|
||||||
|
@ -837,7 +965,10 @@ ContainerWindow.prototype = {
|
||||||
menuItemElement.setAttribute("data-identity-color", identity.color);
|
menuItemElement.setAttribute("data-identity-color", identity.color);
|
||||||
|
|
||||||
menuItemElement.addEventListener("command", (e) => {
|
menuItemElement.addEventListener("command", (e) => {
|
||||||
ContainerService.openTab({userContextId: identity.userContextId});
|
ContainerService.openTab({
|
||||||
|
userContextId: identity.userContextId,
|
||||||
|
source: "tab-bar"
|
||||||
|
});
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -870,7 +1001,10 @@ ContainerWindow.prototype = {
|
||||||
_configureFileMenu() {
|
_configureFileMenu() {
|
||||||
return this._configureMenu("menu_newUserContext", null, e => {
|
return this._configureMenu("menu_newUserContext", null, e => {
|
||||||
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
||||||
ContainerService.openTab({ userContextId });
|
ContainerService.openTab({
|
||||||
|
userContextId: userContextId,
|
||||||
|
source: "file-menu"
|
||||||
|
});
|
||||||
}, "_fileMenuElements");
|
}, "_fileMenuElements");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
"jpm": "^1.2.2",
|
"jpm": "^1.2.2",
|
||||||
"npm-run-all": "^4.0.0",
|
"npm-run-all": "^4.0.0",
|
||||||
"stylelint": "^7.7.1",
|
"stylelint": "^7.7.1",
|
||||||
"stylelint-config-standard": "^15.0.1"
|
"stylelint-config-standard": "^15.0.1",
|
||||||
|
"testpilot-metrics": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"firefox": ">=50.0"
|
"firefox": ">=50.0"
|
||||||
|
|
333
testpilot-metrics.js
Normal file
333
testpilot-metrics.js
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that represents a metrics event broker. Events are sent to Google
|
||||||
|
* Analytics if the `tid` parameter is set. Events are sent to Mozilla's
|
||||||
|
* data pipeline via the Test Pilot add-on. No metrics code changes are
|
||||||
|
* needed when the experiment is added to or removed from Test Pilot.
|
||||||
|
* @constructor
|
||||||
|
* @param {string} $0.id - addon ID, e.g. '@testpilot-addon'. See https://mdn.io/add_on_id.
|
||||||
|
* @param {string} $0.version - addon version, e.g. '1.0.2'.
|
||||||
|
* @param {string} [$0.uid] - unique identifier for a specific instance of an addon.
|
||||||
|
* Optional, but required to send events to Google Analytics. Sent to Google Analytics
|
||||||
|
* but not Mozilla services.
|
||||||
|
* @param {string} [$0.tid] - Google Analytics tracking ID. Optional, but required
|
||||||
|
* to send events to Google Analytics.
|
||||||
|
* @param {string} [$0.type=webextension] - addon type. one of: 'webextension',
|
||||||
|
* 'sdk', 'bootstrapped'.
|
||||||
|
* @param {boolean} [$0.debug=false] - if true, enables logging. Note that this
|
||||||
|
* value can be changed on a running instance, by modifying its `debug` property.
|
||||||
|
* @throws {SyntaxError} If the required properties are missing, or if the
|
||||||
|
* 'type' property is unrecognized.
|
||||||
|
* @throws {Error} if initializing the transports fails.
|
||||||
|
*/
|
||||||
|
function Metrics({id, version, uid, tid = null, type = 'webextension', debug = false}) {
|
||||||
|
if (!id) {
|
||||||
|
throw new SyntaxError(`'id' property is required.`);
|
||||||
|
} else if (!version) {
|
||||||
|
throw new SyntaxError(`'version' property is required.`);
|
||||||
|
} else if (tid && !uid) {
|
||||||
|
throw new SyntaxError(`'uid' property is required to send events to Google Analytics.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['webextension', 'sdk', 'bootstrapped'].includes(type)) {
|
||||||
|
throw new SyntaxError(`'type' property must be one of: 'webextension', 'sdk', or 'bootstrapped'`);
|
||||||
|
}
|
||||||
|
Object.assign(this, {id, uid, version, tid, type, debug});
|
||||||
|
|
||||||
|
// The test pilot add-on uses its own nsIObserverService topic for sending
|
||||||
|
// pings to Telemetry. Otherwise, the topic is based on add-on type.
|
||||||
|
if (id === '@testpilot-addon') {
|
||||||
|
this.topic = 'testpilot';
|
||||||
|
} else if (type === 'webextension') {
|
||||||
|
this.topic = 'testpilot-telemetry';
|
||||||
|
} else {
|
||||||
|
this.topic = 'testpilottest';
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: order is important here. _initTransports uses console.log, which may
|
||||||
|
// not be available before _initConsole has run.
|
||||||
|
this._initConsole();
|
||||||
|
this._initTransports();
|
||||||
|
|
||||||
|
this.sendEvent = this.sendEvent.bind(this);
|
||||||
|
|
||||||
|
this._log(`Initialized topic to ${this.topic}`);
|
||||||
|
if (!tid) {
|
||||||
|
this._log(`Google Analytics disabled: 'tid' value not passed to constructor.`);
|
||||||
|
} else {
|
||||||
|
this._log(`Google Analytics enabled for Tracking ID ${tid}.`);
|
||||||
|
}
|
||||||
|
this._log('Constructor finished successfully.');
|
||||||
|
}
|
||||||
|
Metrics.prototype = {
|
||||||
|
/**
|
||||||
|
* Sends an event to the Mozilla data pipeline (and Google Analytics, if
|
||||||
|
* a `tid` was passed to the constructor). Note: to avoid breaking callers,
|
||||||
|
* if sending the event fails, no Errors will be thrown. Instead, the message
|
||||||
|
* will be silently dropped, and, if debug mode is enabled, an error will be
|
||||||
|
* logged to the Browser Console.
|
||||||
|
*
|
||||||
|
* If you want to pass extra fields to GA, or use a GA hit type other than
|
||||||
|
* `Event`, you can transform the output data object yourself using the
|
||||||
|
* `transform` parameter. You will need to add Custom Dimensions to GA for any
|
||||||
|
* extra fields: https://support.google.com/analytics/answer/2709828. Note
|
||||||
|
* that, by convention, the `variant` argument is mapped to the first Custom
|
||||||
|
* Dimension (`cd1`) when constructing the GA Event hit.
|
||||||
|
*
|
||||||
|
* Note: the data object format is currently different for each experiment,
|
||||||
|
* and should be defined based on the result of conversations with the Mozilla
|
||||||
|
* data team.
|
||||||
|
*
|
||||||
|
* A suggested default format is:
|
||||||
|
* @param {string} [$0.method] - What is happening? e.g. `click`
|
||||||
|
* @param {string} [$0.object] - What is being affected? e.g. `home-button-1`
|
||||||
|
* @param {string} [$0.category=interactions] - If you want to add a category
|
||||||
|
* for easy reporting later. e.g. `mainmenu`
|
||||||
|
* @param {string} [$0.variant=null] - An identifying string if you're running
|
||||||
|
* different variants. e.g. `cohort-A`
|
||||||
|
* @param {function} [transform] - Transform function used to alter the
|
||||||
|
* parameters sent to GA. The `transform` function signature is
|
||||||
|
* `transform(input, output)`, where `input` is the object passed to
|
||||||
|
* `sendEvent` (excluding `transform`), and `output` is the default GA
|
||||||
|
* object generated by the `_gaTransform` method. The `transform` function
|
||||||
|
* should return an object whose keys are GA Measurement Protocol parameters.
|
||||||
|
* The returned object will be form encoded and sent to GA.
|
||||||
|
*/
|
||||||
|
sendEvent: function(params = {}, transform) {
|
||||||
|
const args = this._clone(params);
|
||||||
|
args.object = params.object || null;
|
||||||
|
args.category = params.category || 'interactions';
|
||||||
|
args.variant = params.variant || null;
|
||||||
|
|
||||||
|
this._log(`sendEvent called with method = ${args.method}, object = ${args.object}, category = ${args.category}, variant = ${args.variant}.`);
|
||||||
|
|
||||||
|
const clientData = this._clone(args);
|
||||||
|
const gaData = this._clone(args);
|
||||||
|
if (!clientData) {
|
||||||
|
this._error(`Unable to process data object. Dropping packet.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._sendToClient(clientData);
|
||||||
|
|
||||||
|
if (this.tid && this.uid) {
|
||||||
|
const defaultEvent = this._gaTransform(gaData);
|
||||||
|
|
||||||
|
let userEvent;
|
||||||
|
if (transform) {
|
||||||
|
userEvent = transform.call(null, gaData, defaultEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._gaSend(userEvent || defaultEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a data object by serializing / deserializing it.
|
||||||
|
* @private
|
||||||
|
* @param {object} o - Object to be cloned.
|
||||||
|
* @returns A clone of the object, or `null` if cloning failed.
|
||||||
|
*/
|
||||||
|
_clone: function(o) {
|
||||||
|
let cloned;
|
||||||
|
try {
|
||||||
|
cloned = JSON.parse(JSON.stringify(o));
|
||||||
|
} catch (ex) {
|
||||||
|
this._error(`Unable to clone object: ${ex}.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an event to the Mozilla data pipeline via the Test Pilot add-on.
|
||||||
|
* Uses BroadcastChannel for WebExtensions, and nsIObserverService for other
|
||||||
|
* add-on types.
|
||||||
|
* @private
|
||||||
|
* @param {object} params - Entire object sent to `sendEvent`.
|
||||||
|
*/
|
||||||
|
_sendToClient: function(params) {
|
||||||
|
if (this.type === 'webextension') {
|
||||||
|
this._channel.postMessage(params);
|
||||||
|
this._log(`Sent client message via postMessage: ${params}`);
|
||||||
|
} else {
|
||||||
|
let stringified;
|
||||||
|
|
||||||
|
try {
|
||||||
|
stringified = JSON.stringify(params);
|
||||||
|
} catch(ex) {
|
||||||
|
this._error(`Unable to serialize metrics event: ${ex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = {
|
||||||
|
wrappedJSObject: {
|
||||||
|
observersModuleSubjectWrapper: true,
|
||||||
|
object: this.id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Services.obs.notifyObservers(subject, 'testpilot::send-metric', stringified);
|
||||||
|
this._log(`Sent client message via nsIObserverService: ${stringified}`);
|
||||||
|
} catch (ex) {
|
||||||
|
this._error(`Failed to send nsIObserver client ping: ${ex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms `sendEvent()` arguments into a Google Analytics `Event` hit.
|
||||||
|
* @private
|
||||||
|
* @param {string} method - see `sendEvent` docs
|
||||||
|
* @param {string} [object] - see `sendEvent` docs
|
||||||
|
* @param {string} category - see `sendEvent` docs. Note that `category` is
|
||||||
|
* required here, assuming the default value was filled in by `sendEvent()`.
|
||||||
|
* @param {string} variant - see `sendEvent` docs. Note that `variant` is
|
||||||
|
* required here, assuming the default value was filled in by `sendEvent()`.
|
||||||
|
*/
|
||||||
|
_gaTransform: function({method, object, category, variant}) {
|
||||||
|
const data = {
|
||||||
|
v: 1,
|
||||||
|
an: this.id,
|
||||||
|
av: this.version,
|
||||||
|
tid: this.tid,
|
||||||
|
uid: this.uid,
|
||||||
|
t: 'event',
|
||||||
|
ec: category,
|
||||||
|
ea: method
|
||||||
|
};
|
||||||
|
if (object) {
|
||||||
|
data.el = object;
|
||||||
|
}
|
||||||
|
if (variant) {
|
||||||
|
data.cd1 = variant;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes and sends an event message to Google Analytics.
|
||||||
|
* @private
|
||||||
|
* @param {object} msg - An object whose keys correspond to parameters in the
|
||||||
|
* Google Analytics Measurement Protocol.
|
||||||
|
*/
|
||||||
|
_gaSend: function(msg) {
|
||||||
|
const encoded = this._formEncode(msg);
|
||||||
|
const GA_URL = 'https://ssl.google-analytics.com/collect';
|
||||||
|
if (this.type === 'webextension') {
|
||||||
|
navigator.sendBeacon(GA_URL, encoded);
|
||||||
|
} else {
|
||||||
|
// SDK and bootstrapped types might not have a window reference, so get
|
||||||
|
// the sendBeacon DOM API from the hidden window.
|
||||||
|
Services.appShell.hiddenDOMWindow.navigator.sendBeacon(GA_URL, encoded);
|
||||||
|
}
|
||||||
|
this._log(`Sent GA message: ${encoded}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL encodes an object. Encodes spaces as '%20', not '+', following the
|
||||||
|
* GA docs.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns 'a=b&foo=b%20ar'
|
||||||
|
* metrics._formEncode({a: 'b', foo: 'b ar'});
|
||||||
|
* @private
|
||||||
|
* @param {Object} obj - Any JS object
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_formEncode: function(obj) {
|
||||||
|
const params = [];
|
||||||
|
if (!obj) { return ''; }
|
||||||
|
Object.keys(obj).forEach(item => {
|
||||||
|
const encoded = encodeURIComponent(item) + '=' + encodeURIComponent(obj[item]);
|
||||||
|
params.push(encoded);
|
||||||
|
});
|
||||||
|
return params.join('&');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes transports used for sending messages. For WebExtensions,
|
||||||
|
* creates a `BroadcastChannel` (transport for client pings). WebExtensions
|
||||||
|
* use navigator.sendBeacon for GA transport, and they always have access
|
||||||
|
* to DOM APIs, so there's no setup work required. For other types, loads
|
||||||
|
* `Services.jsm`, which exposes the nsIObserverService (transport for client
|
||||||
|
* pings), and exposes the navigator.sendBeacon API (GA transport) via the
|
||||||
|
* appShell service's hidden window.
|
||||||
|
* @private
|
||||||
|
* @throws {Error} if transport setup unexpectedly fails
|
||||||
|
*/
|
||||||
|
_initTransports: function() {
|
||||||
|
if (this.type === 'webextension') {
|
||||||
|
try {
|
||||||
|
this._channel = new BroadcastChannel(this.topic);
|
||||||
|
} catch(ex) {
|
||||||
|
throw new Error(`Unable to create BroadcastChannel: ${ex}`);
|
||||||
|
}
|
||||||
|
} else if (this.type === 'sdk') {
|
||||||
|
try {
|
||||||
|
const { Cu } = require('chrome');
|
||||||
|
Cu.import('resource://gre/modules/Services.jsm');
|
||||||
|
} catch(ex) {
|
||||||
|
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
||||||
|
}
|
||||||
|
} else { /* this.type === 'bootstrapped' */
|
||||||
|
try {
|
||||||
|
Components.utils.import('resource://gre/modules/Services.jsm');
|
||||||
|
} catch(ex) {
|
||||||
|
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._log('Successfully initialized transports.');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a console for 'bootstrapped' add-ons.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initConsole: function() {
|
||||||
|
if (this.type === 'bootstrapped') {
|
||||||
|
try {
|
||||||
|
Components.utils.import('resource://gre/modules/Console.jsm');
|
||||||
|
this._log('Successfully initialized console.');
|
||||||
|
} catch(ex) {
|
||||||
|
throw new Error(`Unable to initialize console: ${ex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs messages to the console. Only enabled if `this.debug` is truthy.
|
||||||
|
* @private
|
||||||
|
* @param {string} msg - A message
|
||||||
|
*/
|
||||||
|
_log: function(msg) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(msg); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs errors to the console. Only enabled if `this.debug` is truthy.
|
||||||
|
* @private
|
||||||
|
* @param {string} msg - An error message
|
||||||
|
*/
|
||||||
|
_error: function(msg) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.error(msg); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebExtensions don't support CommonJS module style, so 'module' might not be
|
||||||
|
// defined.
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
module.exports = Metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the Metrics constructor in Gecko JSM style, for legacy addons
|
||||||
|
// that use the JSM loader. See also: https://mdn.io/jsm/using
|
||||||
|
const EXPORTED_SYMBOLS = ['Metrics']; // eslint-disable-line no-unused-vars
|
|
@ -203,6 +203,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#edit-containers-link").addEventListener("click", () => {
|
document.querySelector("#edit-containers-link").addEventListener("click", () => {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
method: "sendTelemetryPayload",
|
||||||
|
event: "edit-containers"
|
||||||
|
});
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -246,6 +250,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
return browser.runtime.sendMessage({
|
return browser.runtime.sendMessage({
|
||||||
method: "openTab",
|
method: "openTab",
|
||||||
userContextId: identity.userContextId,
|
userContextId: identity.userContextId,
|
||||||
|
source: "pop-up"
|
||||||
});
|
});
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.close();
|
window.close();
|
||||||
|
|
Loading…
Add table
Reference in a new issue