From 1f95d9767caf6184130400950c770f65c091b09f Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 15 Aug 2017 17:30:27 +0100 Subject: [PATCH] Remove legacy telemetry code as non functional now --- .eslintignore | 2 - .eslintrc.js | 7 +- README.md | 22 +- bootstrap.js | 22 +- lib/shield/event-target.js | 55 --- lib/shield/index.js | 428 ------------------ lib/testpilot/experiment.js | 95 ---- package.json | 6 +- study.js | 40 -- testpilot-metrics.js | 336 -------------- webextension/js/background/assignManager.js | 12 - webextension/js/background/backgroundLogic.js | 65 --- webextension/js/background/messageHandler.js | 6 - webextension/js/background/tabPageCounter.js | 10 - webextension/js/confirm-page.js | 8 - webextension/js/popup.js | 24 +- 16 files changed, 32 insertions(+), 1106 deletions(-) delete mode 100644 lib/shield/event-target.js delete mode 100644 lib/shield/index.js delete mode 100644 lib/testpilot/experiment.js delete mode 100644 study.js delete mode 100644 testpilot-metrics.js diff --git a/.eslintignore b/.eslintignore index 4ced8a0..9f1953c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1 @@ -testpilot-metrics.js -lib/shield/*.js lib/testpilot/*.js diff --git a/.eslintrc.js b/.eslintrc.js index d9f7270..1b0e906 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,12 @@ module.exports = { "CustomizableUI": true, "CustomizableWidgets": true, "SessionStore": true, - "Services": true + "Services": true, + "Components": true, + "XPCOMUtils": true, + "OS": true, + "ADDON_UNINSTALL": true, + "ADDON_DISABLE": true }, "plugins": [ "promise", diff --git a/README.md b/README.md index 027dc58..8ae8c43 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. 5. Click the gear, and select "Install Add-on From File..." 6. Select the `.xpi` file +#### Correct prefs + +Whilst this is still using legacy code to test you will need the following in your profile: + + +Change the following prefs in about:config: + +- extensions.legacy.enabled = true +- xpinstall.signatures.required = false + + #### Run the TxP experiment with `jpm` 1. `git clone git@github.com:mozilla/testpilot-containers.git` @@ -49,23 +60,12 @@ Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code. -#### Run the shield study with `shield` - -1. `git clone git@github.com:mozilla/testpilot-containers.git` -2. `cd testpilot-containers` -3. `npm install` -4. `npm install -g shield-study-cli` -5. `shield run . -- --binary Nightly` - ### Building .xpi To build a local testpilot-containers.xpi, use the plain [`jpm xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command, or run `npm run build`. -#### Building a shield .xpi -To build a local shield-study-containers.xpi, run `npm run build-shield`. - ### Signing an .xpi To sign an .xpi, use [`jpm diff --git a/bootstrap.js b/bootstrap.js index 9fcf037..64e89f0 100644 --- a/bootstrap.js +++ b/bootstrap.js @@ -22,12 +22,11 @@ const PREFS = [ type: "bool" }, ]; -const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -const { TextDecoder, TextEncoder } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {}); +const { TextDecoder, TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); @@ -36,7 +35,7 @@ const JETPACK_DIR_BASENAME = "jetpack"; const EXTENSION_ID = "@testpilot-containers"; function filename() { - let storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile); storeFile.append(JETPACK_DIR_BASENAME); storeFile.append(EXTENSION_ID); storeFile.append("simple-storage"); @@ -46,7 +45,7 @@ function filename() { async function getConfig() { const bytes = await OS.File.read(filename()); - let raw = new TextDecoder().decode(bytes) || ""; + const raw = new TextDecoder().decode(bytes) || ""; let savedConfig = {savedConfiguration: {}}; if (raw) { savedConfig = JSON.parse(raw); @@ -69,7 +68,7 @@ async function initConfig() { }); } const serialized = JSON.stringify(savedConfig); - let bytes = new TextEncoder().encode(serialized) || ""; + const bytes = new TextEncoder().encode(serialized) || ""; await OS.File.writeAtomic(filename(), bytes, { }); } @@ -83,14 +82,16 @@ function setPrefs() { }); } +// eslint-disable-next-line no-unused-vars async function install() { await initConfig(); setPrefs(); } +// eslint-disable-next-line no-unused-vars async function uninstall(aData, aReason) { - if (aReason == ADDON_UNINSTALL - || aReason == ADDON_DISABLE) { + if (aReason === ADDON_UNINSTALL + || aReason === ADDON_DISABLE) { const config = await getConfig(); const storedPrefs = config.savedConfiguration.prefs; PREFS.forEach((pref) => { @@ -105,14 +106,15 @@ async function uninstall(aData, aReason) { } } +// eslint-disable-next-line no-unused-vars function startup({webExtension}) { // Reset prefs that may have changed, or are legacy setPrefs(); // Start the embedded webextension. - webExtension.startup().then(api => { - }); + webExtension.startup(); } -function shutdown(data) { +// eslint-disable-next-line no-unused-vars +function shutdown() { } diff --git a/lib/shield/event-target.js b/lib/shield/event-target.js deleted file mode 100644 index 4335d8c..0000000 --- a/lib/shield/event-target.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Drop-in replacement for {@link external:sdk/event/target.EventTarget} for use - * with es6 classes. - * @module event-target - * @author Martin Giger - * @license MPL-2.0 - */ - /** - * An SDK class that add event reqistration methods - * @external sdk/event/target - * @requires sdk/event/target - */ -/** - * @class EventTarget - * @memberof external:sdk/event/target - * @see {@link https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/event_target#EventTarget} - */ - -// slightly modified from: https://raw.githubusercontent.com/freaktechnik/justintv-stream-notifications/master/lib/event-target.js - -"use strict"; - -const { on, once, off, setListeners } = require("sdk/event/core"); - -/* istanbul ignore next */ -/** - * @class - */ -class EventTarget { - constructor(options) { - setListeners(this, options); - } - - on(...args) { - on(this, ...args); - return this; - } - - once(...args) { - once(this, ...args); - return this; - } - - off(...args) { - off(this, ...args); - return this; - } - - removeListener(...args) { - off(this, ...args); - return this; - } -} - -exports.EventTarget = EventTarget; diff --git a/lib/shield/index.js b/lib/shield/index.js deleted file mode 100644 index 9d4ec95..0000000 --- a/lib/shield/index.js +++ /dev/null @@ -1,428 +0,0 @@ -"use strict"; - -// Chrome privileged -const {Cu} = require("chrome"); -const { Services } = Cu.import("resource://gre/modules/Services.jsm"); -const { TelemetryController } = Cu.import("resource://gre/modules/TelemetryController.jsm"); -const CID = Cu.import("resource://gre/modules/ClientID.jsm"); - -// sdk -const { merge } = require("sdk/util/object"); -const querystring = require("sdk/querystring"); -const { prefs } = require("sdk/simple-prefs"); -const prefSvc = require("sdk/preferences/service"); -const { setInterval } = require("sdk/timers"); -const tabs = require("sdk/tabs"); -const { URL } = require("sdk/url"); - -const { EventTarget } = require("./event-target"); -const { emit } = require("sdk/event/core"); -const self = require("sdk/self"); - -const DAY = 86400*1000; - -// ongoing within-addon fuses / timers -let lastDailyPing = Date.now(); - -/* Functional, self-contained utils */ - -// equal probability choices from a list "choices" -function chooseVariation(choices,rng=Math.random()) { - let l = choices.length; - return choices[Math.floor(l*Math.random())]; -} - -function dateToUTC(date) { - return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); -} - -function generateTelemetryIdIfNeeded() { - let id = TelemetryController.clientID; - /* istanbul ignore next */ - if (id == undefined) { - return CID.ClientIDImpl._doLoadClientID() - } else { - return Promise.resolve(id) - } -} - -function userId () { - return prefSvc.get("toolkit.telemetry.cachedClientID","unknown"); -} - -var Reporter = new EventTarget().on("report", - (d) => prefSvc.get('shield.debug') && console.log("report",d) -); - -function report(data, src="addon", bucket="shield-study") { - data = merge({}, data , { - study_version: self.version, - about: { - _src: src, - _v: 2 - } - }); - if (prefSvc.get('shield.testing')) data.testing = true - - emit(Reporter, "report", data); - let telOptions = {addClientId: true, addEnvironment: true} - return TelemetryController.submitExternalPing(bucket, data, telOptions); -} - -function survey (url, queryArgs={}) { - if (! url) return - - let U = new URL(url); - let q = U.search; - if (q) { - url = U.href.split(q)[0]; - q = querystring.parse(querystring.unescape(q.slice(1))); - } else { - q = {}; - } - // get user info. - let newArgs = merge({}, - q, - queryArgs - ); - let searchstring = querystring.stringify(newArgs); - url = url + "?" + searchstring; - return url; -} - - -function setOrGetFirstrun () { - let firstrun = prefs["shield.firstrun"]; - if (firstrun === undefined) { - firstrun = prefs["shield.firstrun"] = String(dateToUTC(new Date())) // in utc, user set - } - return Number(firstrun) -} - -function reuseVariation (choices) { - return prefs["shield.variation"]; -} - -function setVariation (choice) { - prefs["shield.variation"] = choice - return choice -} - -function die (addonId=self.id) { - /* istanbul ignore else */ - if (prefSvc.get("shield.fakedie")) return; - /* istanbul ignore next */ - require("sdk/addon/installer").uninstall(addonId); -} - -// TODO: GRL vulnerable to clock time issues #1 -function expired (xconfig, now = Date.now() ) { - return ((now - Number(xconfig.firstrun))/ DAY) > xconfig.days; -} - -function resetShieldPrefs () { - delete prefs['shield.firstrun']; - delete prefs['shield.variation']; -} - -function cleanup () { - prefSvc.keys(`extensions.${self.preferencesBranch}`).forEach ( - (p) => { - delete prefs[p]; - }) -} - -function telemetrySubset (xconfig) { - return { - study_name: xconfig.name, - branch: xconfig.variation, - } -} - -class Study extends EventTarget { - constructor (config) { - super(); - this.config = merge({ - name: self.addonId, - variations: {'observe-only': () => {}}, - surveyUrls: {}, - days: 7 - },config); - - this.config.firstrun = setOrGetFirstrun(); - - let variation = reuseVariation(); - if (variation === undefined) { - variation = this.decideVariation(); - if (!(variation in this.config.variations)) { - // chaijs doesn't think this is an instanceof Error - // freaktechnik and gregglind debugged for a while. - // sdk errors might not be 'Errors' or chai is wack, who knows. - // https://dxr.mozilla.org/mozilla-central/search?q=regexp%3AError%5Cs%3F(%3A%7C%3D)+path%3Aaddon-sdk%2Fsource%2F&redirect=false would list - throw new Error("Study Error: chosen variation must be in config.variations") - } - setVariation(variation); - } - this.config.variation = variation; - - this.flags = { - ineligibleDie: undefined - }; - this.states = []; - // all these work, but could be cleaner. I hate the `bind` stuff. - this.on( - "change", (function (newstate) { - prefSvc.get('shield.debug') && console.log(newstate, this.states); - this.states.push(newstate); - emit(this, newstate); // could have checks here. - }).bind(this) - ) - this.on( - "starting", (function () { - this.changeState("modifying"); - }).bind(this) - ) - this.on( - "maybe-installing", (function () { - if (!this.isEligible()) { - this.changeState("ineligible-die"); - } else { - this.changeState("installed") - } - }).bind(this) - ) - this.on( - "ineligible-die", (function () { - try {this.whenIneligible()} catch (err) {/*ok*/} finally { /*ok*/ } - this.flags.ineligibleDie = true; - this.report(merge({}, telemetrySubset(this.config), {study_state: "ineligible"}), "shield"); - this.final(); - die(); - }).bind(this) - ) - this.on( - "installed", (function () { - try {this.whenInstalled()} catch (err) {/*ok*/} finally { /*ok*/ } - this.report(merge({}, telemetrySubset(this.config), {study_state: "install"}), "shield"); - this.changeState("modifying"); - }).bind(this) - ) - this.on( - "modifying", (function () { - var mybranchname = this.variation; - this.config.variations[mybranchname](); // do the effect - this.changeState("running"); - }).bind(this) - ) - this.on( // the one 'many' - "running", (function () { - // report success - this.report(merge({}, telemetrySubset(this.config), {study_state: "running"}), "shield"); - this.final(); - }).bind(this) - ) - this.on( - "normal-shutdown", (function () { - this.flags.dying = true; - this.report(merge({}, telemetrySubset(this.config), {study_state: "shutdown"}), "shield"); - this.final(); - }).bind(this) - ) - this.on( - "end-of-study", (function () { - if (this.flags.expired) { // safe to call multiple times - this.final(); - return; - } else { - // first time seen. - this.flags.expired = true; - try {this.whenComplete()} catch (err) { /*ok*/ } finally { /*ok*/ } - this.report(merge({}, telemetrySubset(this.config) ,{study_state: "end-of-study"}), "shield"); - // survey for end of study - let that = this; - generateTelemetryIdIfNeeded().then(()=>that.showSurvey("end-of-study")); - try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ } - this.final(); - die(); - } - }).bind(this) - ) - this.on( - "user-uninstall-disable", (function () { - if (this.flags.dying) { - this.final(); - return; - } - this.flags.dying = true; - this.report(merge({}, telemetrySubset(this.config), {study_state: "user-ended-study"}), "shield"); - let that = this; - generateTelemetryIdIfNeeded().then(()=>that.showSurvey("user-ended-study")); - try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ } - this.final(); - die(); - }).bind(this) - ) - } - - get state () { - let n = this.states.length; - return n ? this.states[n-1] : undefined - } - - get variation () { - return this.config.variation; - } - - get firstrun () { - return this.config.firstrun; - } - - dieIfExpired () { - let xconfig = this.config; - if (expired(xconfig)) { - emit(this, "change", "end-of-study"); - return true - } else { - return false - } - } - - alivenessPulse (last=lastDailyPing) { - // check for new day, phone home if true. - let t = Date.now(); - if ((t - last) >= DAY) { - lastDailyPing = t; - // phone home - emit(this,"change","running"); - } - // check expiration, and die with report if needed - return this.dieIfExpired(); - } - - changeState (newstate) { - emit(this,'change', newstate); - } - - final () { - emit(this,'final', {}); - } - - startup (reason) { - // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload - - // check expiry first, before anything, quit and die if so - - // check once, right away, short circuit both install and startup - // to prevent modifications from happening. - if (this.dieIfExpired()) return this - - switch (reason) { - case "install": - emit(this, "change", "maybe-installing"); - break; - - case "enable": - case "startup": - case "upgrade": - case "downgrade": - emit(this, "change", "starting"); - } - - if (! this._pulseTimer) this._pulseTimer = setInterval(this.alivenessPulse.bind(this), 5*60*1000 /*5 minutes */) - return this; - } - - shutdown (reason) { - // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload - if (this.flags.ineligibleDie || - this.flags.expired || - this.flags.dying - ) { return this } // special cases. - - switch (reason) { - case "uninstall": - case "disable": - emit(this, "change", "user-uninstall-disable"); - break; - - // 5. usual end of session. - case "shutdown": - case "upgrade": - case "downgrade": - emit(this, "change", "normal-shutdown") - break; - } - return this; - } - - cleanup () { - // do the simple prefs and simplestorage cleanup - // extend by extension - resetShieldPrefs(); - cleanup(); - } - - isEligible () { - return true; - } - - whenIneligible () { - // empty function unless overrided - } - - whenInstalled () { - // empty unless overrided - } - - whenComplete () { - // when the study expires - } - - /** - * equal choice from varations, by default. override to get unequal - */ - decideVariation (rng=Math.random()) { - return chooseVariation(Object.keys(this.config.variations), rng); - } - - get surveyQueryArgs () { - return { - variation: this.variation, - xname: this.config.name, - who: userId(), - updateChannel: Services.appinfo.defaultUpdateChannel, - fxVersion: Services.appinfo.version, - } - } - - showSurvey(reason) { - let partial = this.config.surveyUrls[reason]; - - let queryArgs = this.surveyQueryArgs; - queryArgs.reason = reason; - if (partial) { - let url = survey(partial, queryArgs); - tabs.open(url); - return url - } else { - return - } - } - - report () { // convenience only - return report.apply(null, arguments); - } -} - -module.exports = { - chooseVariation: chooseVariation, - die: die, - expired: expired, - generateTelemetryIdIfNeeded: generateTelemetryIdIfNeeded, - report: report, - Reporter: Reporter, - resetShieldPrefs: resetShieldPrefs, - Study: Study, - cleanup: cleanup, - survey: survey -} diff --git a/lib/testpilot/experiment.js b/lib/testpilot/experiment.js deleted file mode 100644 index 1b4e98f..0000000 --- a/lib/testpilot/experiment.js +++ /dev/null @@ -1,95 +0,0 @@ -const { AddonManager } = require('resource://gre/modules/AddonManager.jsm'); -const { ClientID } = require('resource://gre/modules/ClientID.jsm'); -const Events = require('sdk/system/events'); -const { Services } = require('resource://gre/modules/Services.jsm'); -const { storage } = require('sdk/simple-storage'); -const { - TelemetryController -} = require('resource://gre/modules/TelemetryController.jsm'); -const { Request } = require('sdk/request'); - - -const EVENT_SEND_METRIC = 'testpilot::send-metric'; -const startTime = (Services.startup.getStartupInfo().process); - -function makeTimestamp(timestamp) { - return Math.round((timestamp - startTime) / 1000); -} - -function experimentPing(event) { - const timestamp = new Date(); - const { subject, data } = event; - let parsed; - try { - parsed = JSON.parse(data); - } catch (err) { - // eslint-disable-next-line no-console - return console.error(`Dropping bad metrics packet: ${err}`); - } - - AddonManager.getAddonByID(subject, addon => { - const payload = { - test: subject, - version: addon.version, - timestamp: makeTimestamp(timestamp), - variants: storage.experimentVariants && - subject in storage.experimentVariants - ? storage.experimentVariants[subject] - : null, - payload: parsed - }; - TelemetryController.submitExternalPing('testpilottest', payload, { - addClientId: true, - addEnvironment: true - }); - - // TODO: DRY up this ping centre code here and in lib/Telemetry. - const pcPing = TelemetryController.getCurrentPingData(); - pcPing.type = 'testpilot'; - pcPing.payload = payload; - const pcPayload = { - // 'method' is used by testpilot-metrics library. - // 'event' was used before that library existed. - event_type: parsed.event || parsed.method, - client_time: makeTimestamp(parsed.timestamp || timestamp), - addon_id: subject, - addon_version: addon.version, - firefox_version: pcPing.environment.build.version, - os_name: pcPing.environment.system.os.name, - os_version: pcPing.environment.system.os.version, - locale: pcPing.environment.settings.locale, - // Note: these two keys are normally inserted by the ping-centre client. - client_id: ClientID.getCachedClientID(), - topic: 'testpilot' - }; - // Add any other extra top-level keys = require(the payload, possibly including - // 'object' or 'category', among others. - Object.keys(parsed).forEach(f => { - // Ignore the keys we've already added to `pcPayload`. - const ignored = ['event', 'method', 'timestamp']; - if (!ignored.includes(f)) { - pcPayload[f] = parsed[f]; - } - }); - - const req = new Request({ - url: 'https://tiles.services.mozilla.com/v3/links/ping-centre', - contentType: 'application/json', - content: JSON.stringify(pcPayload) - }); - req.post(); - }); -} - -function Experiment() { - // If the user has @testpilot-addon, it already bound - // experimentPing to testpilot::send-metric, - // so we don't need to bind this one - AddonManager.getAddonByID('@testpilot-addon', addon => { - if (!addon) { - Events.on(EVENT_SEND_METRIC, experimentPing); - } - }); -} - -module.exports = Experiment; diff --git a/package.json b/package.json index fd09b9f..e50ec0d 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,9 @@ "jpm": "^1.2.2", "json": "^9.0.6", "npm-run-all": "^4.0.0", - "shield-studies-addon-utils": "^2.0.0", "stylelint": "^7.9.0", "stylelint-config-standard": "^16.0.0", - "stylelint-order": "^0.3.0", - "testpilot-metrics": "^2.1.0" + "stylelint-order": "^0.3.0" }, "homepage": "https://github.com/mozilla/testpilot-containers#readme", "license": "MPL-2.0", @@ -33,7 +31,6 @@ }, "scripts": { "build": "npm test && jpm xpi", - "build-shield": "npm test && npm run package-shield", "deploy": "deploy-txp", "lint": "npm-run-all lint:*", "lint:addon": "addons-linter webextension --self-hosted", @@ -41,7 +38,6 @@ "lint:html": "htmllint webextension/*.html", "lint:js": "eslint .", "package": "npm run build && mv testpilot-containers.xpi addon.xpi", - "package-shield": "./node_modules/.bin/json -I -f package.json -e 'this.name=\"shield-study-containers\"' && jpm xpi && ./node_modules/.bin/json -I -f package.json -e 'this.name=\"testpilot-containers\"'", "test": "npm run lint" }, "updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json" diff --git a/study.js b/study.js deleted file mode 100644 index df59555..0000000 --- a/study.js +++ /dev/null @@ -1,40 +0,0 @@ -/* 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/. */ - -const self = require("sdk/self"); -const { when: unload } = require("sdk/system/unload"); - -const shield = require("./lib/shield/index"); - -const surveyUrl = "https://www.surveygizmo.com/s3/3621810/shield-txp-containers"; - -const studyConfig = { - name: self.addonId, - days: 28, - surveyUrls: { - "end-of-study": surveyUrl, - "user-ended-study": surveyUrl, - ineligible: null, - }, - variations: { - "control": () => {}, - "securityOnboarding": () => {} - } -}; - -class ContainersStudy extends shield.Study { - isEligible () { - // If the user already has testpilot-containers extension, they are in the - // Test Pilot experiment, so exclude them. - return super.isEligible(); - } -} - -const thisStudy = new ContainersStudy(studyConfig); - -if (self.id === "@shield-study-containers") { - unload((reason) => thisStudy.shutdown(reason)); -} - -exports.study = thisStudy; diff --git a/testpilot-metrics.js b/testpilot-metrics.js deleted file mode 100644 index 2914884..0000000 --- a/testpilot-metrics.js +++ /dev/null @@ -1,336 +0,0 @@ -// 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/. -const Experiment = require('./lib/testpilot/experiment'); - -const experiment = new Experiment(); - -/** - * 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 diff --git a/webextension/js/background/assignManager.js b/webextension/js/background/assignManager.js index e356c6d..72d1dc2 100644 --- a/webextension/js/background/assignManager.js +++ b/webextension/js/background/assignManager.js @@ -234,10 +234,6 @@ const assignManager = { browser.tabs.sendMessage(tabId, { text: `Successfully ${actionName} site to always open in this container` }); - backgroundLogic.sendTelemetryPayload({ - event: `${actionName}-container-assignment`, - userContextId: userContextId, - }); const tab = await browser.tabs.get(tabId); this.calculateContextMenu(tab); }, @@ -298,15 +294,7 @@ const assignManager = { // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there if (neverAsk) { browser.tabs.create({url, cookieStoreId, index}); - backgroundLogic.sendTelemetryPayload({ - event: "auto-reload-page-in-container", - userContextId: userContextId, - }); } else { - backgroundLogic.sendTelemetryPayload({ - event: "prompt-to-reload-page-in-container", - userContextId: userContextId, - }); let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`; let currentCookieStoreId; if (currentUserContextId) { diff --git a/webextension/js/background/backgroundLogic.js b/webextension/js/background/backgroundLogic.js index 42ee382..b0d0355 100644 --- a/webextension/js/background/backgroundLogic.js +++ b/webextension/js/background/backgroundLogic.js @@ -26,11 +26,6 @@ const backgroundLogic = { }, async deleteContainer(userContextId) { - this.sendTelemetryPayload({ - event: "delete-container", - userContextId - }); - await this._closeTabs(userContextId); await browser.contextualIdentities.remove(this.cookieStoreId(userContextId)); assignManager.deleteContainer(userContextId); @@ -47,15 +42,8 @@ const backgroundLogic = { this.cookieStoreId(options.userContextId), options.params ); - this.sendTelemetryPayload({ - event: "edit-container", - userContextId: options.userContextId - }); } else { donePromise = browser.contextualIdentities.create(options.params); - this.sendTelemetryPayload({ - event: "add-container" - }); } await donePromise; browser.runtime.sendMessage({ @@ -67,18 +55,8 @@ const backgroundLogic = { let url = options.url || undefined; const userContextId = ("userContextId" in options) ? options.userContextId : 0; const active = ("nofocus" in options) ? options.nofocus : true; - const source = ("source" in options) ? options.source : null; const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); - // Only send telemetry for tabs opened by UI - i.e., not via showTabs - if (source && userContextId) { - this.sendTelemetryPayload({ - "event": "open-tab", - "eventSource": source, - "userContextId": userContextId, - "clickedContainerTabCount": await identityState.containerTabCount(cookieStoreId) - }); - } // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 // We can't open new tab pages, so open a blank tab. Used in tab un-hide @@ -131,12 +109,6 @@ const backgroundLogic = { return null; } - this.sendTelemetryPayload({ - "event": "move-tabs-to-window", - "userContextId": userContextId, - "clickedContainerTabCount": identityState.containerTabCount(userContextId), - }); - const list = await identityState._matchTabsByContainer(options.cookieStoreId); const containerState = await identityState.storageArea.get(options.cookieStoreId); @@ -199,13 +171,6 @@ const backgroundLogic = { }, async sortTabs() { - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "sort-tabs", - "shownContainersCount": containersCounts.shown, - "totalContainerTabsCount": await identityState.totalContainerTabsCount(), - "totalNonContainerTabsCount": await identityState.totalNonContainerTabsCount() - }); const windows = await browser.windows.getAll(); for (let window of windows) { // eslint-disable-line prefer-const // First the pinned tabs, then the normal ones. @@ -267,16 +232,6 @@ const backgroundLogic = { return null; } - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "hide-tabs", - "userContextId": userContextId, - "clickedContainerTabCount": identityState.containerTabCount(userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total - }); - const containerState = await identityState.storeHidden(options.cookieStoreId); await this._closeTabs(userContextId); return containerState; @@ -293,16 +248,6 @@ const backgroundLogic = { return null; } - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "show-tabs", - "userContextId": userContextId, - "clickedContainerTabCount": await identityState.containerTabCount(options.cookieStoreId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total - }); - const promises = []; const containerState = await identityState.storageArea.get(options.cookieStoreId); @@ -322,16 +267,6 @@ const backgroundLogic = { return await identityState.storageArea.set(options.cookieStoreId, containerState); }, - - sendTelemetryPayload(message = {}) { - if (!message.event) { - throw new Error("Missing event name for telemetry"); - } - message.method = "sendTelemetryPayload"; - //TODO decide where this goes - // browser.runtime.sendMessage(message); - }, - cookieStoreId(userContextId) { return `firefox-container-${userContextId}`; }, diff --git a/webextension/js/background/messageHandler.js b/webextension/js/background/messageHandler.js index 4f96891..d675b19 100644 --- a/webextension/js/background/messageHandler.js +++ b/webextension/js/background/messageHandler.js @@ -38,9 +38,6 @@ const messageHandler = { return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value); }); break; - case "sendTelemetryPayload": - // TODO - break; case "sortTabs": backgroundLogic.sortTabs(); break; @@ -53,9 +50,6 @@ const messageHandler = { case "checkIncompatibleAddons": // TODO break; - case "getShieldStudyVariation": - // TODO - break; case "moveTabsToWindow": response = backgroundLogic.moveTabsToWindow({ cookieStoreId: m.cookieStoreId diff --git a/webextension/js/background/tabPageCounter.js b/webextension/js/background/tabPageCounter.js index b0b6272..19d9591 100644 --- a/webextension/js/background/tabPageCounter.js +++ b/webextension/js/background/tabPageCounter.js @@ -34,20 +34,10 @@ const tabPageCounter = { return; } if (why === "user-closed-tab" && this.counters[tabId].tab) { - backgroundLogic.sendTelemetryPayload({ - event: "page-requests-completed-per-tab", - userContextId: this.counters[tabId].tab.cookieStoreId, - pageRequestCount: this.counters[tabId].tab.pageRequests - }); // When we send the ping because the user closed the tab, // delete both the 'tab' and 'activity' counters delete this.counters[tabId]; } else if (why === "user-went-idle" && this.counters[tabId].activity) { - backgroundLogic.sendTelemetryPayload({ - event: "page-requests-completed-per-activity", - userContextId: this.counters[tabId].activity.cookieStoreId, - pageRequestCount: this.counters[tabId].activity.pageRequests - }); // When we send the ping because the user went idle, // only reset the 'activity' counter this.counters[tabId].activity = { diff --git a/webextension/js/confirm-page.js b/webextension/js/confirm-page.js index aa8c9a2..fef08d4 100644 --- a/webextension/js/confirm-page.js +++ b/webextension/js/confirm-page.js @@ -49,10 +49,6 @@ function confirmSubmit(redirectUrl, cookieStoreId) { pageUrl: redirectUrl }); } - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", - event: "click-to-reload-page-in-container", - }); openInContainer(redirectUrl, cookieStoreId); } @@ -70,10 +66,6 @@ async function denySubmit(redirectUrl) { tabId: tab[0].id, pageUrl: redirectUrl }); - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", - event: "click-to-reload-page-in-same-container", - }); document.location.replace(redirectUrl); } diff --git a/webextension/js/popup.js b/webextension/js/popup.js index a68f19a..82f9658 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -81,11 +81,9 @@ const Logic = { // Retrieve the list of identities. const identitiesPromise = this.refreshIdentities(); - // Get the onboarding variation - const variationPromise = this.getShieldStudyVariation(); try { - await Promise.all([identitiesPromise, variationPromise]); + await identitiesPromise; } catch(e) { throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message); } @@ -158,7 +156,7 @@ const Logic = { }; // Handle old style rejection with null and also Promise.reject new style try { - return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer; + return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer; } catch(e) { return defaultContainer; } @@ -274,14 +272,6 @@ const Logic = { return identity.cookieStoreId; }, - sendTelemetryPayload(message = {}) { - if (!message.event) { - throw new Error("Missing event name for telemetry"); - } - message.method = "sendTelemetryPayload"; - browser.runtime.sendMessage(message); - }, - removeIdentity(userContextId) { if (!userContextId) { return Promise.reject("removeIdentity must be called with userContextId argument."); @@ -317,13 +307,6 @@ const Logic = { }); }, - async getShieldStudyVariation() { - const variation = await browser.runtime.sendMessage({ - method: "getShieldStudyVariation" - }); - this._onboardingVariation = variation; - }, - generateIdentityName() { const defaultName = "Container #"; const ids = []; @@ -474,9 +457,6 @@ Logic.registerPanel(P_CONTAINERS_LIST, { }); Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => { - Logic.sendTelemetryPayload({ - event: "edit-containers" - }); Logic.showPanel(P_CONTAINERS_EDIT); });