From aaf62903c5f2c290b5b56109ae537e827d9d4599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Wollse=CC=81n?= Date: Sat, 10 Mar 2018 11:29:26 +0200 Subject: [PATCH] Results of npm run format --- .eslintrc.js | 18 +- package.json | 4 +- shield-study-helper-addon/addon/bootstrap.js | 41 +- .../addon/webextension/.eslintrc.json | 20 +- .../addon/webextension/qa.js | 20 +- shield-study-helper-addon/run-firefox.js | 27 +- src/StudyUtils.in.jsm | 606 +++++++++--------- src/schema.studySetup.json | 35 +- src/schema.webExtensionMsg.json | 11 +- src/schema.weightedVariations.json | 5 +- src/schemas.js | 12 +- test-addon/bootstrap.js | 6 +- test-addon/utils.jsm | 15 +- test/shield_utils_test.js | 203 ++++-- test/utils.js | 32 +- webpack.config.js | 14 +- 16 files changed, 575 insertions(+), 494 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6a20de7..c22875c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,23 +2,17 @@ module.exports = { env: { - "node": true + node: true, }, - extends: [ - "eslint:recommended", - "plugin:mozilla/recommended", - ], + extends: ["eslint:recommended", "plugin:mozilla/recommended"], - plugins: [ - "mozilla", - "json" - ], + plugins: ["mozilla", "json"], rules: { "babel/new-cap": "off", "comma-dangle": ["error", "always-multiline"], - "eqeqeq": "error", - "indent": ["warn", 2, {SwitchCase: 1}], + eqeqeq: "error", + indent: ["warn", 2, { SwitchCase: 1 }], "mozilla/no-aArgs": "warn", "mozilla/balanced-listeners": 0, "no-console": "warn", @@ -26,7 +20,7 @@ module.exports = { "no-shadow": ["error"], "no-unused-vars": "error", "prefer-const": "warn", - "semi": ["error", "always"], + semi: ["error", "always"], "require-jsdoc": "warn", "valid-jsdoc": "warn", "max-len": ["error", 80], diff --git a/package.json b/package.json index aab0d75..18a5261 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "scripts": { "build-test-addon-xpi": "./bin/make_xpi.sh", "dist": "webpack", + "docformat": "doctoc --title '**Contents**' docs/*.md && prettier '**/*.md' --write", "eslint": "eslint src --ext jsm --ext js --ext json", "eslint-fix": "npm run eslint -- --fix", "format": "prettier '**/*.{css,js,jsm,json,md}' --trailing-comma=all --ignore-path=.eslintignore --write", @@ -53,7 +54,6 @@ "predist": "npm run eslint", "prepack": "fixpack && npm run dist", "pretest": "npm run dist && npm run build-test-addon-xpi", - "test": "export FIREFOX_BINARY=firefox && XPI_NAME=test-addon/test-addon.xpi mocha test", - "docformat": "doctoc --title '**Contents**' docs/*.md && prettier '**/*.md' --write" + "test": "export FIREFOX_BINARY=firefox && XPI_NAME=test-addon/test-addon.xpi mocha test" } } diff --git a/shield-study-helper-addon/addon/bootstrap.js b/shield-study-helper-addon/addon/bootstrap.js index 7db1e9b..efcf49a 100644 --- a/shield-study-helper-addon/addon/bootstrap.js +++ b/shield-study-helper-addon/addon/bootstrap.js @@ -3,15 +3,15 @@ /* global __SCRIPT_URI_SPEC__, Feature, studyUtils, config */ /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(startup|shutdown|install|uninstall)" }]*/ -async function getTelemetryPings (options) { +async function getTelemetryPings(options) { // type is String or Array - const {type, n, timestamp, headersOnly} = options; + const { type, n, timestamp, headersOnly } = options; Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); // {type, id, timestampCreated} let pings = await TelemetryArchive.promiseArchivedPingList(); if (type) { if (!(type instanceof Array)) { - type = [type]; // Array-ify if it's a string + type = [type]; // Array-ify if it's a string } } if (type) pings = pings.filter(p => type.includes(p.type)); @@ -19,23 +19,26 @@ async function getTelemetryPings (options) { pings.sort((a, b) => b.timestampCreated - a.timestampCreated); if (n) pings = pings.slice(0, n); - const pingData = headersOnly ? pings : pings.map(ping => TelemetryArchive.promiseArchivedPingById(ping.id)); - return Promise.all(pingData) + const pingData = headersOnly + ? pings + : pings.map(ping => TelemetryArchive.promiseArchivedPingById(ping.id)); + return Promise.all(pingData); } -async function pingsReport () { +async function pingsReport() { async function getPings() { const ar = ["shield-study", "shield-study-addon"]; - return getTelemetryPings({type: ["shield-study", "shield-study-addon"]}); + return getTelemetryPings({ type: ["shield-study", "shield-study-addon"] }); } const pings = (await getPings()).reverse(); if (pings.length == 0) { - return {"report": "No pings found"} + return { report: "No pings found" }; } const p0 = pings[0].payload; // print common fields - const report = ` + const report = + ` // common fields branch ${p0.branch} // should describe Question text @@ -43,20 +46,23 @@ study_name ${p0.study_name} addon_version ${p0.addon_version} version ${p0.version} -` + pings.map((p,i)=>`${i} ${p.creationDate} ${p.payload.type} -${JSON.stringify(p.payload.data,null,2)} +` + + pings + .map( + (p, i) => `${i} ${p.creationDate} ${p.payload.type} +${JSON.stringify(p.payload.data, null, 2)} -`).join('\n'); +`, + ) + .join("\n"); - return {"report": report}; + return { report: report }; //pings.forEach(p=>{ // console.log(p.creationDate, p.payload.type); // console.log(JSON.stringify(p.payload.data,null,2)) //}) } - - async function listenFromWebExtension(msg, sender, sendResponse) { //await pingsReport(); console.log(`got ${msg}`); @@ -76,12 +82,11 @@ async function listenFromWebExtension(msg, sender, sendResponse) { return false; } - async function startup(addonData, reason) { - console.log('starting up debugger') + console.log("starting up debugger"); const webExtension = addonData.webExtension; webExtension.startup().then(api => { - const {browser} = api; + const { browser } = api; // messages intended for shieldn: {shield:true,msg=[info|endStudy|telemetry],data=data} browser.runtime.onMessage.addListener(listenFromWebExtension); // other message handlers from your addon, if any diff --git a/shield-study-helper-addon/addon/webextension/.eslintrc.json b/shield-study-helper-addon/addon/webextension/.eslintrc.json index 1d71c50..5c7cc9e 100644 --- a/shield-study-helper-addon/addon/webextension/.eslintrc.json +++ b/shield-study-helper-addon/addon/webextension/.eslintrc.json @@ -1,13 +1,11 @@ { - "env": { - "browser": true, - "es6": true, - "webextensions": true - }, - "extends": [ - "eslint:recommended" - ], - "rules": { - "no-console": "warn" - } + "env": { + "browser": true, + "es6": true, + "webextensions": true + }, + "extends": ["eslint:recommended"], + "rules": { + "no-console": "warn" + } } diff --git a/shield-study-helper-addon/addon/webextension/qa.js b/shield-study-helper-addon/addon/webextension/qa.js index ef036a1..d9f3142 100644 --- a/shield-study-helper-addon/addon/webextension/qa.js +++ b/shield-study-helper-addon/addon/webextension/qa.js @@ -7,26 +7,25 @@ has a bunch of pings and stuff `; -function printReport (text) { +function printReport(text) { console.log(`about to replace: ${text}`); - document.querySelector('#timestamp').textContent=`${new Date()}`; - document.querySelector('#qa').textContent=text; + document.querySelector("#timestamp").textContent = `${new Date()}`; + document.querySelector("#qa").textContent = text; } - -async function tryReportFromFirefox () { - console.log(`has browser runtime? ${browser.runtime}`) +async function tryReportFromFirefox() { + console.log(`has browser runtime? ${browser.runtime}`); if (browser.runtime) { - const reply = await browser.runtime.sendMessage("qa-report") + const reply = await browser.runtime.sendMessage("qa-report"); console.log("got reply!", reply); if (reply) { printReport(reply.report); //console.log("response from legacy add-on: " + reply.content); - }; + } } } -function startup () { +function startup() { printReport(PLACEHOLDER); console.log("asking firefox"); tryReportFromFirefox(); @@ -39,5 +38,4 @@ page starts up. - once it arrives, insert it. */ - -document.addEventListener('DOMContentLoaded', startup); +document.addEventListener("DOMContentLoaded", startup); diff --git a/shield-study-helper-addon/run-firefox.js b/shield-study-helper-addon/run-firefox.js index c784775..0307240 100644 --- a/shield-study-helper-addon/run-firefox.js +++ b/shield-study-helper-addon/run-firefox.js @@ -10,7 +10,6 @@ console.log("Starting up firefox"); - require("geckodriver"); const firefox = require("selenium-webdriver/firefox"); const cmd = require("selenium-webdriver/lib/command"); @@ -60,11 +59,11 @@ async function promiseActualBinary(binary) { } } -promiseSetupDriver = async() => { +promiseSetupDriver = async () => { const profile = new firefox.Profile(); // TODO, allow 'actually send telemetry' here. - Object.keys(FIREFOX_PREFERENCES).forEach((key) => { + Object.keys(FIREFOX_PREFERENCES).forEach(key => { profile.setPreference(key, FIREFOX_PREFERENCES[key]); }); @@ -76,7 +75,9 @@ promiseSetupDriver = async() => { .forBrowser("firefox") .setFirefoxOptions(options); - const binaryLocation = await promiseActualBinary(process.env.FIREFOX_BINARY || "nightly"); + const binaryLocation = await promiseActualBinary( + process.env.FIREFOX_BINARY || "nightly", + ); await options.setBinary(new firefox.Binary(binaryLocation)); const driver = await builder.build(); // Firefox will be started up by now @@ -85,21 +86,28 @@ promiseSetupDriver = async() => { return driver; }; -installAddon = async(driver, fileLocation) => { +installAddon = async (driver, fileLocation) => { // references: // https://bugzilla.mozilla.org/show_bug.cgi?id=1298025 // https://github.com/mozilla/geckodriver/releases/tag/v0.17.0 const executor = driver.getExecutor(); - executor.defineCommand("installAddon", "POST", "/session/:sessionId/moz/addon/install"); + executor.defineCommand( + "installAddon", + "POST", + "/session/:sessionId/moz/addon/install", + ); const installCmd = new cmd.Command("installAddon"); const session = await driver.getSession(); - installCmd.setParameters({ sessionId: session.getId(), path: fileLocation, temporary: true }); + installCmd.setParameters({ + sessionId: session.getId(), + path: fileLocation, + temporary: true, + }); return executor.execute(installCmd); }; - -(async() => { +(async () => { try { const driver = await promiseSetupDriver(); @@ -114,7 +122,6 @@ installAddon = async(driver, fileLocation) => { // navigate to a regular page driver.setContext(Context.CONTENT); driver.get("about:debugging"); - } catch (e) { console.error(e); // eslint-disable-line no-console } diff --git a/src/StudyUtils.in.jsm b/src/StudyUtils.in.jsm index f4a5e88..af2e835 100644 --- a/src/StudyUtils.in.jsm +++ b/src/StudyUtils.in.jsm @@ -10,7 +10,6 @@ * config data passed in is valid per the studySetup schema. */ - /* * TODO glind survey / urls & query args * TODO glind publish as v4 @@ -20,7 +19,7 @@ const EXPORTED_SYMBOLS = ["studyUtils"]; const UTILS_VERSION = require("../package.json").version; const PACKET_VERSION = 3; -const {utils: Cu} = Components; +const { utils: Cu } = Components; Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -30,10 +29,14 @@ let log; // telemetry utils const CID = Cu.import("resource://gre/modules/ClientID.jsm", null); -const { TelemetryController } - = Cu.import("resource://gre/modules/TelemetryController.jsm", null); -const { TelemetryEnvironment } - = Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", null); +const { TelemetryController } = Cu.import( + "resource://gre/modules/TelemetryController.jsm", + null, +); +const { TelemetryEnvironment } = Cu.import( + "resource://gre/modules/TelemetryEnvironment.jsm", + null, +); /* * Set-up JSON schema validation @@ -61,25 +64,27 @@ const ajv = new Ajv(); var jsonschema = { /** - * Validates input data based on a specified schema - * @param {Object} data - The data to be validated - * @param {Object} schema - The schema to validate against - * @returns {boolean} - Will return true if the data is valid - */ + * Validates input data based on a specified schema + * @param {Object} data - The data to be validated + * @param {Object} schema - The schema to validate against + * @returns {boolean} - Will return true if the data is valid + */ validate(data, schema) { var valid = ajv.validate(schema, data); - return {valid, errors: ajv.errors || []}; + return { valid, errors: ajv.errors || [] }; }, /** - * Validates input data based on a specified schema - * @param {Object} data - The data to be validated - * @param {Object} schema - The schema to validate against - * @throws Will throw an error if the data is not valid - * @returns {boolean} - Will return true if the data is valid - */ + * Validates input data based on a specified schema + * @param {Object} data - The data to be validated + * @param {Object} schema - The schema to validate against + * @throws Will throw an error if the data is not valid + * @returns {boolean} - Will return true if the data is valid + */ validateOrThrow(data, schema) { const valid = ajv.validate(schema, data); - if (!valid) { throw new Error(JSON.stringify((ajv.errors))); } + if (!valid) { + throw new Error(JSON.stringify(ajv.errors)); + } return true; }, }; @@ -89,7 +94,7 @@ var jsonschema = { * Probably deeper than we need. Unlike the shallow merge with the * spread operator (const c = {...a, ...b}), this function can be configured * to copy non-enumerable properties, symbols, and property descriptors. - * + * * Merges all the properties of all arguments into first argument. If two or * more argument objects have own properties with the same name, the property * is overridden, with precedence from right to left, implying, that properties @@ -118,29 +123,32 @@ function merge(source) { nonEnumerables: true, }; /** - * Gets object's own property symbols and/or names, including non-enumerables - * by default - * @param {Object} object - the object for which to get own property symbols - * and names - * @param {Object} options - object indicating what kinds of properties to - * merge - * @param {boolean} options.name - True if function should return object's own - * property names - * @param {boolean} options.symbols - True if function should return object's - * own property symbols - * @param {boolean} options.nonEnumerables - True if function should return - * object's non-enumerable own property names - * @returns {string[]|symbol[]} - An array of own property names and/or symbols - * for object - */ + * Gets object's own property symbols and/or names, including non-enumerables + * by default + * @param {Object} object - the object for which to get own property symbols + * and names + * @param {Object} options - object indicating what kinds of properties to + * merge + * @param {boolean} options.name - True if function should return object's own + * property names + * @param {boolean} options.symbols - True if function should return + * object's own property symbols + * @param {boolean} options.nonEnumerables - True if function should return + * object's non-enumerable own property names + * @returns {string[]|symbol[]} - An array of own property names and/or + * symbols for object + */ function getOwnPropertyIdentifiers(object, options = optionsDefault) { - const symbols = !options.symbols ? [] : - Object.getOwnPropertySymbols(object); + const symbols = !options.symbols + ? [] + : Object.getOwnPropertySymbols(object); // eslint-disable-next-line - const names = !options.names ? [] : - options.nonEnumerables ? Object.getOwnPropertyNames(object) : - Object.keys(object); + const names = !options.names + ? [] + : options.nonEnumerables + ? Object.getOwnPropertyNames(object) + : Object.keys(object); return [...names, ...symbols]; } /* @@ -154,21 +162,23 @@ function merge(source) { * converted to `true` where `null` and `undefined` becames `false`. Therefore * the `filter` method will keep only objects that are defined and not null. */ - Array.slice(arguments, 1).filter(Boolean).forEach((properties) => { - getOwnPropertyIdentifiers(properties).forEach((name) => { - descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + Array.slice(arguments, 1) + .filter(Boolean) + .forEach(properties => { + getOwnPropertyIdentifiers(properties).forEach(name => { + descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + }); }); - }); return Object.defineProperties(source, descriptor); } /** -* Appends a query string to a url. -* @param {string} url - a base url to append; must be static (data) or external -* @param {Object} args - query arguments, one or more object literal used to -* build a query string -* @returns {string} - an absolute url appended with a query string -*/ + * Appends a query string to a url. + * @param {string} url - a base url to append; must be static (data) or external + * @param {Object} args - query arguments, one or more object literal used to + * build a query string + * @returns {string} - an absolute url appended with a query string + */ function mergeQueryArgs(url, ...args) { // currently left to right // TODO, glind, decide order of merge here @@ -181,7 +191,7 @@ function mergeQueryArgs(url, ...args) { const merged = merge({}, ...args); // Set each search parameter in "merged" to its value in the query string, // building up the query string one search parameter at a time. - Object.keys(merged).forEach((k) => { + Object.keys(merged).forEach(k => { // k, the search parameter (ex: fxVersion) // q.get(k), returns the value of k, in query string, q (ex: 57.0.1a) log.debug(q.get(k), k, merged[k]); @@ -195,13 +205,13 @@ function mergeQueryArgs(url, ...args) { // sampling utils /** -* @async -* Converts a string into its sha256 hexadecimal representation. -* Note: This is ultimately used to make a hash of the user's telemetry clientID -* and the study name. -* @param {string} message - The message to convert. -* @returns {string} - a hexadecimal, 256-bit hash -*/ + * @async + * Converts a string into its sha256 hexadecimal representation. + * Note: This is ultimately used to make a hash of the user's telemetry clientID + * and the study name. + * @param {string} message - The message to convert. + * @returns {string} - a hexadecimal, 256-bit hash + */ async function sha256(message) { // encode as UTF-8 const msgBuffer = new TextEncoder("utf-8").encode(message); @@ -210,41 +220,42 @@ async function sha256(message) { // convert ArrayBuffer to Array const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert bytes to hex string - const hashHex - = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join(""); + const hashHex = hashArray + .map(b => ("00" + b.toString(16)).slice(-2)) + .join(""); return hashHex; } /** -* Converts an array of length N into a cumulative sum array of length N, -* where n_i = sum(array.slice(0,i)) i.e. each element is the sum of all -* elements up to and including that element -* This is ultimately used for turning sample weights (AKA weightedVariations) -* into right hand limits (>= X) to deterministically select which variation -* a user receives. -* @example [.25,.3,.45] => [.25,.55,1.0]; if a user's sample weight were .25, -* they would fall into the left-most bucket -* @param {Number[]} arr - An array of sample weights (0 <= sample weight < 1) -* @returns {Number[]} - A cumulative sum array of sample weights -* (0 <= sample weight <= 1) -*/ + * Converts an array of length N into a cumulative sum array of length N, + * where n_i = sum(array.slice(0,i)) i.e. each element is the sum of all + * elements up to and including that element + * This is ultimately used for turning sample weights (AKA weightedVariations) + * into right hand limits (>= X) to deterministically select which variation + * a user receives. + * @example [.25,.3,.45] => [.25,.55,1.0]; if a user's sample weight were .25, + * they would fall into the left-most bucket + * @param {Number[]} arr - An array of sample weights (0 <= sample weight < 1) + * @returns {Number[]} - A cumulative sum array of sample weights + * (0 <= sample weight <= 1) + */ function cumsum(arr) { return arr.reduce(function(r, c, i) { r.push((r[i - 1] || 0) + c); return r; - }, [] ); + }, []); } /** -* Given sample weights (weightedVariations) and a particular position -* (fraction), return a variation. If no fraction given, return a variation -* at random fraction proportional to the weightVariations object -* @param {Object[]} weightedVariations - the array of branch name:weight pairs -* used to randomly assign the user to a branch -* @param {Number} fraction - a number (0 <= fraction < 1) -* @returns {Object} - the variation object in weightedVariations for the given -* fraction -*/ + * Given sample weights (weightedVariations) and a particular position + * (fraction), return a variation. If no fraction given, return a variation + * at random fraction proportional to the weightVariations object + * @param {Object[]} weightedVariations - the array of branch name:weight pairs + * used to randomly assign the user to a branch + * @param {Number} fraction - a number (0 <= fraction < 1) + * @returns {Object} - the variation object in weightedVariations for the given + * fraction + */ function chooseWeighted(weightedVariations, fraction = Math.random()) { /* weightedVaiations, list of: @@ -267,87 +278,89 @@ function chooseWeighted(weightedVariations, fraction = Math.random()) { } /** -* @async -* Converts a string into a fraction (0 <= fraction < 1) based on the first -* X bits of its sha256 hexadecimal representation -* Note: Salting (adding the study name to the telemetry clientID) ensures -* that the same user gets a different bucket/hash for each study. -* Hashing of the salted string ensures uniform hashing; i.e. that every -* bucket/variation gets filled. -* @param {string} saltedString - a salted string used to create a hash for -* the user -* @param {Number} bits - The first number of bits to use in the sha256 hex -* representation -* @returns {Number} - a fraction (0 <= fraction < 1) -*/ + * @async + * Converts a string into a fraction (0 <= fraction < 1) based on the first + * X bits of its sha256 hexadecimal representation + * Note: Salting (adding the study name to the telemetry clientID) ensures + * that the same user gets a different bucket/hash for each study. + * Hashing of the salted string ensures uniform hashing; i.e. that every + * bucket/variation gets filled. + * @param {string} saltedString - a salted string used to create a hash for + * the user + * @param {Number} bits - The first number of bits to use in the sha256 hex + * representation + * @returns {Number} - a fraction (0 <= fraction < 1) + */ async function hashFraction(saltedString, bits = 12) { const hash = await sha256(saltedString); return parseInt(hash.substr(0, bits), 16) / Math.pow(16, bits); } /** -* Class representing utilities for shield studies. -*/ + * Class representing utilities for shield studies. + */ class StudyUtils { /** - * Create a StudyUtils instance. - */ + * Create a StudyUtils instance. + */ constructor() { /* * TODO glind Answer: no. see if you can merge the construtor and setup * and export the class, rather than a singleton */ /** - * Handles a message received by the webExtension, sending a response back. - * @param {Object} webExtensionMsg object, see its schema - * @param {boolean} webExtensionMsg.shield - Whether or not the message - * is a shield message (intended for StudyUtils) - * @param {string} webExtensionMsg.msg - StudyUtils method to be called - *from the webExtension - * @param {*} webExtensionMsg.data - Data sent from webExtension - * @param {Object} sender - Details about the message sender, see - * runtime.onMessage MDN docs - * @param {responseCallback} sendResponse - The callback to send a response - * back to the webExtension - * @returns {boolean|undefined} - true if the message has been processed - * (shield message) or ignored (non-shield message) - */ - this.respondToWebExtensionMessage - = function({shield, msg, data}, sender, sendResponse) { - // @TODO glind: make sure we're using the webExtensionMsg schema - if (!shield) return true; - const allowedMethods = ["endStudy", "telemetry", "info"]; - if (!allowedMethods.includes(msg)) { - const errStr1 = "respondToWebExtensionMessage:"; - const errStr2 = "is not in allowed studyUtils methods:"; - throw new Error(`${errStr1} "${msg}" ${errStr2} ${allowedMethods}`); - } - /* + * Handles a message received by the webExtension, sending a response back. + * @param {Object} webExtensionMsg object, see its schema + * @param {boolean} webExtensionMsg.shield - Whether or not the message + * is a shield message (intended for StudyUtils) + * @param {string} webExtensionMsg.msg - StudyUtils method to be called + *from the webExtension + * @param {*} webExtensionMsg.data - Data sent from webExtension + * @param {Object} sender - Details about the message sender, see + * runtime.onMessage MDN docs + * @param {responseCallback} sendResponse - The callback to send a response + * back to the webExtension + * @returns {boolean|undefined} - true if the message has been processed + * (shield message) or ignored (non-shield message) + */ + this.respondToWebExtensionMessage = function( + { shield, msg, data }, + sender, + sendResponse, + ) { + // @TODO glind: make sure we're using the webExtensionMsg schema + if (!shield) return true; + const allowedMethods = ["endStudy", "telemetry", "info"]; + if (!allowedMethods.includes(msg)) { + const errStr1 = "respondToWebExtensionMessage:"; + const errStr2 = "is not in allowed studyUtils methods:"; + throw new Error(`${errStr1} "${msg}" ${errStr2} ${allowedMethods}`); + } + /* * handle async * Execute the StudyUtils method requested by the webExtension * then send the webExtension a response with their return value */ - Promise.resolve(this[msg](data)).then( - function(ans) { - log.debug("respondingTo", msg, ans); - sendResponse(ans); - }, - // function error eventually - ); - return true; - /* Ensure this method is bound to the instance of studyUtils, see + Promise.resolve(this[msg](data)).then( + function(ans) { + log.debug("respondingTo", msg, ans); + sendResponse(ans); + }, + // function error eventually + ); + return true; + /* Ensure this method is bound to the instance of studyUtils, see * callsite in bootstrap.js * TODO glind: bdanforth's claim: making this function a StudyUtils * method would also do this. */ - }.bind(this); + }.bind(this); /* * Expose sampling methods onto the exported studyUtils singleton, for use * by any Components.utils-importing module */ this.sample = { - sha256, cumsum, chooseWeighted, @@ -363,21 +376,22 @@ class StudyUtils { } /** - * Checks if the StudyUtils.setup method has been called - * @param {string} name - the name of a StudyUtils method - * @returns {void} - */ + * Checks if the StudyUtils.setup method has been called + * @param {string} name - the name of a StudyUtils method + * @returns {void} + */ throwIfNotSetup(name = "unknown") { - if (!this._isSetup) throw new Error( - name + ": this method can't be used until `setup` is called" - ); + if (!this._isSetup) + throw new Error( + name + ": this method can't be used until `setup` is called", + ); } /** - * Validates the studySetup object passed in from the addon. - * @param {Object} config - the studySetup object, see schema.studySetup.json - * @returns {StudyUtils} - the StudyUtils class instance - */ + * Validates the studySetup object passed in from the addon. + * @param {Object} config - the studySetup object, see schema.studySetup.json + * @returns {StudyUtils} - the StudyUtils class instance + */ setup(config) { log = createLog("shield-study-utils", config.log.studyUtils.level); @@ -390,9 +404,9 @@ class StudyUtils { } /** - * Resets the state of the study. Suggested use is for testing. - * @returns {void} - */ + * Resets the state of the study. Suggested use is for testing. + * @returns {void} + */ reset() { this.config = {}; delete this._variation; @@ -400,13 +414,13 @@ class StudyUtils { } /** - * @async - * Opens a new tab that loads a page with the specified URL. - * @param {string} url - the url of a page - * @param {Object} params - optional, see - * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/addTab - * @returns {void} - */ + * @async + * Opens a new tab that loads a page with the specified URL. + * @param {string} url - the url of a page + * @param {Object} params - optional, see + * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/addTab + * @returns {void} + */ async openTab(url, params = {}) { this.throwIfNotSetup("openTab"); log.debug(url, params); @@ -419,15 +433,16 @@ class StudyUtils { */ await new Promise(resolve => setTimeout(resolve, 30000)); } - Services.wm.getMostRecentWindow("navigator:browser") + Services.wm + .getMostRecentWindow("navigator:browser") .gBrowser.addTab(url, params); } /** - * @async - * Gets the telemetry client ID for the user. - * @returns {string} - the telemetry client ID - */ + * @async + * Gets the telemetry client ID for the user. + * @returns {string} - the telemetry client ID + */ async getTelemetryId() { const id = TelemetryController.clientID; /* istanbul ignore next */ @@ -438,10 +453,10 @@ class StudyUtils { } /** - * Sets the variation for the StudyUtils instance. - * @param {Object} variation - the study variation for this user - * @returns {StudyUtils} - the StudyUtils class instance - */ + * Sets the variation for the StudyUtils instance. + * @param {Object} variation - the study variation for this user + * @returns {StudyUtils} - the StudyUtils class instance + */ setVariation(variation) { this.throwIfNotSetup("setVariation"); this._variation = variation; @@ -449,22 +464,22 @@ class StudyUtils { } /** - * Gets the variation for the StudyUtils instance. - * @returns {Object} - the study variation for this user - */ + * Gets the variation for the StudyUtils instance. + * @returns {Object} - the study variation for this user + */ getVariation() { this.throwIfNotSetup("getvariation"); return this._variation; } /** - * @async - * Deterministically selects and returns the study variation for the user. - * @param {Object[]} weightedVariations - see schema.weightedVariations.json - * @param {Number} rng - randomly generated number (0 <= rng < 1) of the form - * returned by Math.random; can be set explicitly for testing - * @returns {Object} - the study variation for this user - */ + * @async + * Deterministically selects and returns the study variation for the user. + * @param {Object[]} weightedVariations - see schema.weightedVariations.json + * @param {Number} rng - randomly generated number (0 <= rng < 1) of the form + * returned by Math.random; can be set explicitly for testing + * @returns {Object} - the study variation for this user + */ async deterministicVariation(weightedVariations, rng = null) { // hash the studyName and telemetryId to get the same branch every time. this.throwIfNotSetup("deterministicVariation needs studyName"); @@ -479,18 +494,18 @@ class StudyUtils { } /** - * Gets the Shield recipe client ID. - * @returns {string} - the Shield recipe client ID. - */ + * Gets the Shield recipe client ID. + * @returns {string} - the Shield recipe client ID. + */ getShieldId() { const key = "extensions.shield-recipe-client.user_id"; return Services.prefs.getCharPref(key, ""); } /** - * Packages information about the study into an object. - * @returns {Object} - study information, see schema.studySetup.json - */ + * Packages information about the study into an object. + * @returns {Object} - study information, see schema.studySetup.json + */ info() { log.debug("getting info"); this.throwIfNotSetup("info"); @@ -503,9 +518,9 @@ class StudyUtils { } /** - * Get the telemetry configuration for the study. - * @returns {Object} - the telemetry cofiguration, see schema.studySetup.json - */ + * Get the telemetry configuration for the study. + * @returns {Object} - the telemetry cofiguration, see schema.studySetup.json + */ // TODO glind, maybe this is getter / setter? get telemetryConfig() { this.throwIfNotSetup("telemetryConfig"); @@ -513,57 +528,57 @@ class StudyUtils { } /** - * Sends an 'enter' telemetry ping for the study; should be called on addon - * startup for the reason ADDON_INSTALL. For more on study states like 'enter' - * see ABOUT.md at github.com/mozilla/shield-studies-addon-template - * @returns {void} - */ + * Sends an 'enter' telemetry ping for the study; should be called on addon + * startup for the reason ADDON_INSTALL. For more on study states like 'enter' + * see ABOUT.md at github.com/mozilla/shield-studies-addon-template + * @returns {void} + */ firstSeen() { log.debug(`firstSeen`); this.throwIfNotSetup("firstSeen uses telemetry."); - this._telemetry({study_state: "enter"}, "shield-study"); + this._telemetry({ study_state: "enter" }, "shield-study"); } /** - * Marks the study's telemetry pings as being part of this experimental - * cohort in a way that downstream data pipeline tools (like ExperimentsViewer) - * can use it. - * @returns {void} - */ + * Marks the study's telemetry pings as being part of this experimental + * cohort in a way that downstream data pipeline tools + * (like ExperimentsViewer) can use it. + * @returns {void} + */ setActive() { this.throwIfNotSetup("setActive uses telemetry."); const info = this.info(); log.debug( "marking TelemetryEnvironment", info.studyName, - info.variation.name + info.variation.name, ); TelemetryEnvironment.setExperimentActive( info.studyName, - info.variation.name + info.variation.name, ); } /** - * Removes the study from the active list of telemetry experiments - * @returns {void} - */ + * Removes the study from the active list of telemetry experiments + * @returns {void} + */ unsetActive() { this.throwIfNotSetup("unsetActive uses telemetry."); const info = this.info(); log.debug( "unmarking TelemetryEnvironment", info.studyName, - info.variation.name + info.variation.name, ); TelemetryEnvironment.setExperimentInactive(info.studyName); } /** - * Uninstalls the shield study addon, given its addon id. - * @param {string} id - the addon id - * @returns {void} - */ + * Uninstalls the shield study addon, given its addon id. + * @param {string} id - the addon id + * @returns {void} + */ uninstall(id) { if (!id) id = this.info().addon.id; if (!id) { @@ -574,37 +589,37 @@ class StudyUtils { } /** - * @async - * Adds the study to the active list of telemetry experiments and sends the - * "installed" telemetry ping if applicable - * @param {string} reason - The reason the addon has started up - * @returns {void} - */ - async startup({reason}) { + * @async + * Adds the study to the active list of telemetry experiments and sends the + * "installed" telemetry ping if applicable + * @param {string} reason - The reason the addon has started up + * @returns {void} + */ + async startup({ reason }) { this.throwIfNotSetup("startup"); log.debug(`startup ${reason}`); this.setActive(); if (reason === REASONS.ADDON_INSTALL) { - this._telemetry({study_state: "installed"}, "shield-study"); + this._telemetry({ study_state: "installed" }, "shield-study"); } } /** - * @async - * Ends the study: - * - Removes the study from the active list of telemetry experiments - * - Opens a new tab at a specified URL, if present (e.g. for a survey) - * - Sends a telemetry ping about the nature of the ending - * (positive, neutral, negative) - * - Sends an exit telemetry ping - * @param {Object} param - A details object describing why the study is ending - * @param {string} param.reason - The reason the study is ending, see - * schema.studySetup.json - * @param {string} param.fullname - optional, the full name of the study - * state, see schema.studySetup.json - * @returns {void} - */ - async endStudy({reason, fullname}) { + * @async + * Ends the study: + * - Removes the study from the active list of telemetry experiments + * - Opens a new tab at a specified URL, if present (e.g. for a survey) + * - Sends a telemetry ping about the nature of the ending + * (positive, neutral, negative) + * - Sends an exit telemetry ping + * @param {Object} param - A details object describing why the study is ending + * @param {string} param.reason - The reason the study is ending, see + * schema.studySetup.json + * @param {string} param.fullname - optional, the full name of the study + * state, see schema.studySetup.json + * @returns {void} + */ + async endStudy({ reason, fullname }) { this.throwIfNotSetup("endStudy"); if (this._isEnding) { log.debug("endStudy, already ending!"); @@ -623,7 +638,7 @@ class StudyUtils { if (ending) { // baseUrl: needs to be appended with query arguments before use, // exactUrl: used as is - const {baseUrl, exactUrl} = ending; + const { baseUrl, exactUrl } = ending; if (exactUrl) { this.openTab(exactUrl); } else if (baseUrl) { @@ -642,7 +657,7 @@ class StudyUtils { case "ended-positive": case "ended-neutral": case "ended-negative": - this._telemetry({study_state: reason, fullname}, "shield-study"); + this._telemetry({ study_state: reason, fullname }, "shield-study"); break; default: this._telemetry( @@ -650,21 +665,21 @@ class StudyUtils { study_state: "ended-neutral", study_state_fullname: reason, }, - "shield-study" + "shield-study", ); - // unless we know better TODO grl + // unless we know better TODO grl } // these are all exits - this._telemetry({study_state: "exit"}, "shield-study"); + this._telemetry({ study_state: "exit" }, "shield-study"); this.uninstall(); // TODO glind. should be controllable by arg? } /** - * @async - * Builds an object whose properties are query arguments that can be - * appended to a study ending url - * @returns {Object} - the query arguments for the study - */ + * @async + * Builds an object whose properties are query arguments that can be + * appended to a study ending url + * @returns {Object} - the query arguments for the study + */ async endingQueryArgs() { // TODO glind, make this back breaking! this.throwIfNotSetup("endingQueryArgs"); @@ -684,29 +699,29 @@ class StudyUtils { } /** - * @async - * Validates and submits telemetry pings from StudyUtils. - * @param {Object} data - the data to send as part of the telemetry packet - * @param {string} bucket - the type of telemetry packet to be sent - * @returns {Promise|boolean} - A promise that resolves with the ping id - * once the ping is stored or sent, or false if - * - there is a validation error, - * - the packet is of type "shield-study-error" - * - the study's telemetryConfig.send is set to false - */ + * @async + * Validates and submits telemetry pings from StudyUtils. + * @param {Object} data - the data to send as part of the telemetry packet + * @param {string} bucket - the type of telemetry packet to be sent + * @returns {Promise|boolean} - A promise that resolves with the ping id + * once the ping is stored or sent, or false if + * - there is a validation error, + * - the packet is of type "shield-study-error" + * - the study's telemetryConfig.send is set to false + */ async _telemetry(data, bucket = "shield-study-addon") { log.debug(`telemetry in: ${bucket} ${JSON.stringify(data)}`); this.throwIfNotSetup("_telemetry"); const info = this.info(); const payload = { - version: PACKET_VERSION, - study_name: info.studyName, - branch: info.variation.name, - addon_version: info.addon.version, + version: PACKET_VERSION, + study_name: info.studyName, + branch: info.variation.name, + addon_version: info.addon.version, shield_version: UTILS_VERSION, - type: bucket, + type: bucket, data, - testing: !this.telemetryConfig.removeTestingFlag, + testing: !this.telemetryConfig.removeTestingFlag, }; let validation; @@ -727,10 +742,10 @@ class StudyUtils { */ if (validation.errors.length) { const errorReport = { - "error_id": "jsonschema-validation", - "error_source": "addon", - "severity": "fatal", - "message": JSON.stringify(validation.errors), + error_id: "jsonschema-validation", + error_source: "addon", + severity: "fatal", + message: JSON.stringify(validation.errors), }; if (bucket === "shield-study-error") { // log: if it's a warn or error, it breaks jpm test @@ -743,7 +758,7 @@ class StudyUtils { log.debug(`telemetry: ${JSON.stringify(payload)}`); // FIXME marcrowo: addClientId makes the ping not appear in test? // seems like a problem with Telemetry, not the shield-study-utils library - const telOptions = {addClientId: true, addEnvironment: true}; + const telOptions = { addClientId: true, addEnvironment: true }; if (!this.telemetryConfig.send) { log.debug("NOT sending. `telemetryConfig.send` is false"); return false; @@ -752,12 +767,12 @@ class StudyUtils { } /** - * @async - * Validates and submits telemetry pings from the addon; mostly from - * webExtension messages. - * @param {Object} data - the data to send as part of the telemetry packet - * @returns {Promise|boolean} - see StudyUtils._telemetry - */ + * @async + * Validates and submits telemetry pings from the addon; mostly from + * webExtension messages. + * @param {Object} data - the data to send as part of the telemetry packet + * @returns {Promise|boolean} - see StudyUtils._telemetry + */ async telemetry(data) { log.debug(`telemetry ${JSON.stringify(data)}`); const toSubmit = { @@ -768,38 +783,37 @@ class StudyUtils { } /** - * Submits error report telemetry pings. - * @param {Object} errorReport - the error report, see StudyUtils._telemetry - * @returns {Promise|boolean} - see StudyUtils._telemetry - */ + * Submits error report telemetry pings. + * @param {Object} errorReport - the error report, see StudyUtils._telemetry + * @returns {Promise|boolean} - see StudyUtils._telemetry + */ telemetryError(errorReport) { return this._telemetry(errorReport, "shield-study-error"); } /** - * Sets the logging level. This is can be called from the addon, even after the - * log has been created. - * @param {string} descriptor - the Log level (e.g. "trace", "error", ...) - * @returns {void} - */ + * Sets the logging level. This is can be called from the addon, even + * after the log has been created. + * @param {string} descriptor - the Log level (e.g. "trace", "error", ...) + * @returns {void} + */ setLoggingLevel(descriptor) { log.level = Log.Level[descriptor]; } - } /** -* Creates a log for debugging. -* Note: Log.jsm is used over Console.log/warn/error because: -* - Console has limited log levels -* - Console is not pref-controllable. Log can be turned on and off using -* config.log (see ./addon/Config.jsm in -* github.com/mozilla/shield-study-addon-template) -* - Console can create linting errors and warnings. -* @param {string} name - the name of the Logger instance -* @param {string} levelWord - the Log level (e.g. "trace", "error", ...) -* @returns {Object} - the Logger instance, see gre/modules/Log.jsm -*/ + * Creates a log for debugging. + * Note: Log.jsm is used over Console.log/warn/error because: + * - Console has limited log levels + * - Console is not pref-controllable. Log can be turned on and off using + * config.log (see ./addon/Config.jsm in + * github.com/mozilla/shield-study-addon-template) + * - Console can create linting errors and warnings. + * @param {string} name - the name of the Logger instance + * @param {string} levelWord - the Log level (e.g. "trace", "error", ...) + * @returns {Object} - the Logger instance, see gre/modules/Log.jsm + */ function createLog(name, levelWord) { Cu.import("resource://gre/modules/Log.jsm"); var L = Log.repository.getLogger(name); @@ -812,16 +826,18 @@ function createLog(name, levelWord) { // addon state change reasons const REASONS = { - APP_STARTUP: 1, // The application is starting up. - APP_SHUTDOWN: 2, // The application is shutting down. - ADDON_ENABLE: 3, // The add-on is being enabled. - ADDON_DISABLE: 4, // The add-on is being disabled. (Also sent at uninstall) - ADDON_INSTALL: 5, // The add-on is being installed. - ADDON_UNINSTALL: 6, // The add-on is being uninstalled. - ADDON_UPGRADE: 7, // The add-on is being upgraded. - ADDON_DOWNGRADE: 8, // The add-on is being downgraded. + APP_STARTUP: 1, // The application is starting up. + APP_SHUTDOWN: 2, // The application is shutting down. + ADDON_ENABLE: 3, // The add-on is being enabled. + ADDON_DISABLE: 4, // The add-on is being disabled. (Also sent at uninstall) + ADDON_INSTALL: 5, // The add-on is being installed. + ADDON_UNINSTALL: 6, // The add-on is being uninstalled. + ADDON_UPGRADE: 7, // The add-on is being upgraded. + ADDON_DOWNGRADE: 8, // The add-on is being downgraded. }; -for (const r in REASONS) { REASONS[REASONS[r]] = r; } +for (const r in REASONS) { + REASONS[REASONS[r]] = r; +} // Actually create the singleton. var studyUtils = new StudyUtils(); diff --git a/src/schema.studySetup.json b/src/schema.studySetup.json index a061b96..ef4616a 100644 --- a/src/schema.studySetup.json +++ b/src/schema.studySetup.json @@ -1,10 +1,10 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "study setup", + "title": "study setup", "type": "object", "definitions": { "idString": { - "description": "between 1,100 chars, no spaces, unicode ok.", + "description": "between 1,100 chars, no spaces, unicode ok.", "type": "string", "pattern": "^\\S+$", "minLength": 1, @@ -27,7 +27,8 @@ }, "study_state_fullname": { "type": "string", - "description": "Second part of name of state, if any. Study-specific for study-defined endings." + "description": + "Second part of name of state, if any. Study-specific for study-defined endings." }, "url": { "type": "string" @@ -60,23 +61,13 @@ }, "onInvalid": { "type": "string", - "enum": [ - "throw", - "log" - ] + "enum": ["throw", "log"] } }, - "required": [ - "removeTestingFlag", - "send" - ] + "required": ["removeTestingFlag", "send"] } }, - "required": [ - "studyName", - "endings", - "telemetry" - ] + "required": ["studyName", "endings", "telemetry"] }, "addon": { "type": "object", @@ -90,14 +81,8 @@ "description": "Semantic version of the addon." } }, - "required": [ - "id", - "version" - ] + "required": ["id", "version"] } }, - "required": [ - "study", - "addon" - ] -} \ No newline at end of file + "required": ["study", "addon"] +} diff --git a/src/schema.webExtensionMsg.json b/src/schema.webExtensionMsg.json index 5c5814f..c5df40b 100644 --- a/src/schema.webExtensionMsg.json +++ b/src/schema.webExtensionMsg.json @@ -8,18 +8,11 @@ }, "msg": { "type": "string", - "enum": [ - "endStudy", - "telemetry", - "info" - ] + "enum": ["endStudy", "telemetry", "info"] }, "data": { "type": "object" } }, - "required": [ - "shield", - "msg" - ] + "required": ["shield", "msg"] } diff --git a/src/schema.weightedVariations.json b/src/schema.weightedVariations.json index f6b7cd5..3c7c809 100644 --- a/src/schema.weightedVariations.json +++ b/src/schema.weightedVariations.json @@ -9,10 +9,7 @@ "type": "number", "minimum": 0 }, - "required": [ - "name", - "weight" - ] + "required": ["name", "weight"] } }, "items": { diff --git a/src/schemas.js b/src/schemas.js index 27ecb3d..17dd1b6 100644 --- a/src/schemas.js +++ b/src/schemas.js @@ -1,8 +1,8 @@ module.exports = { - "shield-study": require("shield-study-schemas/schemas-client/shield-study.schema.json"), - "shield-study-addon": require("shield-study-schemas/schemas-client/shield-study-addon.schema.json"), - "shield-study-error": require("shield-study-schemas/schemas-client/shield-study-error.schema.json"), - "studySetup": require("./schema.studySetup.json"), - "webExtensionMsg": require("./schema.webExtensionMsg.json"), - "weightedVariations": require("./schema.weightedVariations.json"), + "shield-study": require("shield-study-schemas/schemas-client/shield-study.schema.json"), // eslint-disable-line max-len + "shield-study-addon": require("shield-study-schemas/schemas-client/shield-study-addon.schema.json"), // eslint-disable-line max-len + "shield-study-error": require("shield-study-schemas/schemas-client/shield-study-error.schema.json"), // eslint-disable-line max-len + studySetup: require("./schema.studySetup.json"), + webExtensionMsg: require("./schema.webExtensionMsg.json"), + weightedVariations: require("./schema.weightedVariations.json"), }; diff --git a/test-addon/bootstrap.js b/test-addon/bootstrap.js index 4bccfe5..fb3845d 100644 --- a/test-addon/bootstrap.js +++ b/test-addon/bootstrap.js @@ -1,6 +1,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "studyUtils", "resource://test-addon/StudyUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter( + this, + "studyUtils", + "resource://test-addon/StudyUtils.jsm", +); this.install = function(data, reason) {}; diff --git a/test-addon/utils.jsm b/test-addon/utils.jsm index 1472083..1468391 100644 --- a/test-addon/utils.jsm +++ b/test-addon/utils.jsm @@ -1,6 +1,9 @@ /* eslint no-unused-vars: "off" */ -const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); +const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, +); Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); Components.utils.import("resource://gre/modules/Console.jsm"); @@ -11,8 +14,8 @@ function fakeSetup() { study: { studyName: "shield-utils-test", endings: { - "expired": { - "baseUrl": "http://www.example.com/?reason=expired", + expired: { + baseUrl: "http://www.example.com/?reason=expired", }, }, telemetry: { send: true, removeTestingFlag: false }, @@ -23,7 +26,7 @@ function fakeSetup() { level: "Warn", }, }, - addon: {id: "1", version: "1"}, + addon: { id: "1", version: "1" }, }); studyUtils.setVariation({ name: "puppers", weight: "2" }); } @@ -36,6 +39,8 @@ async function getMostRecentPingsByType(type) { const filteredPings = pings.filter(p => p.type === type); filteredPings.sort((a, b) => b.timestampCreated - a.timestampCreated); - const pingData = filteredPings.map(ping => TelemetryArchive.promiseArchivedPingById(ping.id)); + const pingData = filteredPings.map(ping => + TelemetryArchive.promiseArchivedPingById(ping.id), + ); return Promise.all(pingData); } diff --git a/test/shield_utils_test.js b/test/shield_utils_test.js index d54b2ec..41ea5b4 100644 --- a/test/shield_utils_test.js +++ b/test/shield_utils_test.js @@ -14,7 +14,7 @@ describe("Shield Study Utils Functional Tests", function() { let driver; - before(async() => { + before(async () => { driver = await utils.promiseSetupDriver(); // install the addon (note: returns addon id) await utils.installAddon(driver); @@ -22,56 +22,84 @@ describe("Shield Study Utils Functional Tests", function() { after(() => driver.quit()); - it("should return the correct variation", async() => { - const variation = await driver.executeAsyncScript(async(callback) => { - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); + it("should return the correct variation", async () => { + const variation = await driver.executeAsyncScript(async callback => { + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); // TODO move this to a Config.jsm file const studyConfig = { studyName: "shieldStudyUtilsTest", - "weightedVariations": [ - {"name": "control", - "weight": 1}, - {"name": "kittens", - "weight": 1.5}, - {"name": "puppers", - "weight": 2}, // we want more puppers in our sample + weightedVariations: [ + { + name: "control", + weight: 1, + }, + { + name: "kittens", + weight: 1.5, + }, + { + name: "puppers", + weight: 2, + }, // we want more puppers in our sample ], }; const sample = studyUtils.sample; const hashFraction = await sample.hashFraction("test"); - const chosenVariation = await sample.chooseWeighted(studyConfig.weightedVariations, hashFraction); + const chosenVariation = await sample.chooseWeighted( + studyConfig.weightedVariations, + hashFraction, + ); callback(chosenVariation); }); assert(variation.name === "puppers"); }); - it("telemetry should be working", async() => { - const shieldTelemetryPing = await driver.executeAsyncScript(async(callback) => { - const { fakeSetup, getMostRecentPingsByType } = Components.utils.import("resource://test-addon/utils.jsm", {}); - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); - Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); + it("telemetry should be working", async () => { + const shieldTelemetryPing = await driver.executeAsyncScript( + async callback => { + const { fakeSetup, getMostRecentPingsByType } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); + Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); - fakeSetup(); + fakeSetup(); - await studyUtils.telemetry({ "foo": "bar" }); + await studyUtils.telemetry({ foo: "bar" }); - // TODO Fix this hackiness; caused by addClientId option in submitExternalPing - // The ping seems to be sending (appears in about:telemetry) but does not appear - // in the pings array - await new Promise(resolve => setTimeout(resolve, 1000)); + // TODO Fix this hackiness; caused by addClientId option in submitExternalPing + // The ping seems to be sending (appears in about:telemetry) but does not appear + // in the pings array + await new Promise(resolve => setTimeout(resolve, 1000)); - const shieldPings = await getMostRecentPingsByType("shield-study-addon"); - callback(shieldPings[0]); - }); + const shieldPings = await getMostRecentPingsByType( + "shield-study-addon", + ); + callback(shieldPings[0]); + }, + ); assert(shieldTelemetryPing.payload.data.attributes.foo === "bar"); }); - describe("test the library's \"startup\" process", function() { - it("should send the correct ping on first seen", async() => { - const firstSeenPing = await driver.executeAsyncScript(async(callback) => { - const { fakeSetup, getMostRecentPingsByType } = Components.utils.import("resource://test-addon/utils.jsm", {}); - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); + describe('test the library\'s "startup" process', function() { + it("should send the correct ping on first seen", async () => { + const firstSeenPing = await driver.executeAsyncScript(async callback => { + const { fakeSetup, getMostRecentPingsByType } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); fakeSetup(); @@ -84,30 +112,46 @@ describe("Shield Study Utils Functional Tests", function() { assert(firstSeenPing.payload.data.study_state === "enter"); }); - it("should set the experiment to active in Telemetry", async() => { - const activeExperiments = await driver.executeAsyncScript(async(callback) => { - const { fakeSetup } = Components.utils.import("resource://test-addon/utils.jsm", {}); - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); - Components.utils.import("resource://gre/modules/TelemetryEnvironment.jsm"); - - fakeSetup(); - - studyUtils.setActive(); - - callback(TelemetryEnvironment.getActiveExperiments()); - }); + it("should set the experiment to active in Telemetry", async () => { + const activeExperiments = await driver.executeAsyncScript( + async callback => { + const { fakeSetup } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); + Components.utils.import( + "resource://gre/modules/TelemetryEnvironment.jsm", + ); + + fakeSetup(); + + studyUtils.setActive(); + + callback(TelemetryEnvironment.getActiveExperiments()); + }, + ); assert(activeExperiments.hasOwnProperty("shield-utils-test")); }); - it("should send the correct telemetry ping on first install", async() => { - const installedPing = await driver.executeAsyncScript(async(callback) => { - const { fakeSetup, getMostRecentPingsByType } = Components.utils.import("resource://test-addon/utils.jsm", {}); - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); + it("should send the correct telemetry ping on first install", async () => { + const installedPing = await driver.executeAsyncScript(async callback => { + const { fakeSetup, getMostRecentPingsByType } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); fakeSetup(); - await studyUtils.startup({reason: 5}); // ADDON_INSTALL = 5 + await studyUtils.startup({ reason: 5 }); // ADDON_INSTALL = 5 const studyPings = await getMostRecentPingsByType("shield-study"); callback(studyPings[0]); @@ -117,37 +161,50 @@ describe("Shield Study Utils Functional Tests", function() { }); describe("test the library's endStudy() function", function() { - before(async() => { - await driver.executeAsyncScript(async(callback) => { - const { fakeSetup } = Components.utils.import("resource://test-addon/utils.jsm", {}); - const { studyUtils } = Components.utils.import("resource://test-addon/StudyUtils.jsm", {}); + before(async () => { + await driver.executeAsyncScript(async callback => { + const { fakeSetup } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); + const { studyUtils } = Components.utils.import( + "resource://test-addon/StudyUtils.jsm", + {}, + ); fakeSetup(); // TODO add tests for other reasons (?) - await studyUtils.endStudy({reason: "expired", fullname: "TEST_FULLNAME"}); + await studyUtils.endStudy({ + reason: "expired", + fullname: "TEST_FULLNAME", + }); callback(); }); }); - it("should set the experiment as inactive", async() => { - const activeExperiments = await driver.executeAsyncScript(async(callback) => { - Components.utils.import("resource://gre/modules/TelemetryEnvironment.jsm"); - callback(TelemetryEnvironment.getActiveExperiments()); - }); + it("should set the experiment as inactive", async () => { + const activeExperiments = await driver.executeAsyncScript( + async callback => { + Components.utils.import( + "resource://gre/modules/TelemetryEnvironment.jsm", + ); + callback(TelemetryEnvironment.getActiveExperiments()); + }, + ); assert(!activeExperiments.hasOwnProperty("shield-utils-test")); }); describe("test the opening of a URL at the end of the study", function() { - it("should open a new tab", async() => { - const newTabOpened = await driver.wait(async() => { + it("should open a new tab", async () => { + const newTabOpened = await driver.wait(async () => { const handles = await driver.getAllWindowHandles(); return handles.length === 2; // opened a new tab }, 3000); assert(newTabOpened); }); - it("should open a new tab to the correct URL", async() => { + it("should open a new tab to the correct URL", async () => { const currentHandle = await driver.getWindowHandle(); driver.setContext(Context.CONTENT); // Find the new window handle. @@ -158,27 +215,35 @@ describe("Shield Study Utils Functional Tests", function() { newWindowHandle = handle; } } - const correctURLOpened = await driver.wait(async() => { + const correctURLOpened = await driver.wait(async () => { await driver.switchTo().window(newWindowHandle); const currentURL = await driver.getCurrentUrl(); - return currentURL.startsWith("http://www.example.com/?reason=expired"); + return currentURL.startsWith( + "http://www.example.com/?reason=expired", + ); }); assert(correctURLOpened); }); }); - it("should send the correct reason telemetry", async() => { - const pings = await driver.executeAsyncScript(async(callback) => { - const { getMostRecentPingsByType } = Components.utils.import("resource://test-addon/utils.jsm", {}); + it("should send the correct reason telemetry", async () => { + const pings = await driver.executeAsyncScript(async callback => { + const { getMostRecentPingsByType } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); const studyPings = await getMostRecentPingsByType("shield-study"); callback(studyPings[1]); // ping before the most recent ping }); assert(pings.payload.data.study_state === "expired"); }); - it("should send the uninstall telemetry", async() => { - const pings = await driver.executeAsyncScript(async(callback) => { - const { getMostRecentPingsByType } = Components.utils.import("resource://test-addon/utils.jsm", {}); + it("should send the uninstall telemetry", async () => { + const pings = await driver.executeAsyncScript(async callback => { + const { getMostRecentPingsByType } = Components.utils.import( + "resource://test-addon/utils.jsm", + {}, + ); const studyPings = await getMostRecentPingsByType("shield-study"); callback(studyPings[0]); }); diff --git a/test/utils.js b/test/utils.js index f06a237..8722d83 100644 --- a/test/utils.js +++ b/test/utils.js @@ -27,7 +27,7 @@ const FIREFOX_PREFERENCES = { "devtools.chrome.enabled": true, "devtools.debugger.remote-enabled": true, "devtools.debugger.prompt-connection": false, - "general.warnOnAboutConfig": false + "general.warnOnAboutConfig": false, }; // useful if we need to test on a specific version of Firefox @@ -44,10 +44,10 @@ async function promiseActualBinary(binary) { } } -module.exports.promiseSetupDriver = async() => { +module.exports.promiseSetupDriver = async () => { const profile = new firefox.Profile(); - Object.keys(FIREFOX_PREFERENCES).forEach((key) => { + Object.keys(FIREFOX_PREFERENCES).forEach(key => { profile.setPreference(key, FIREFOX_PREFERENCES[key]); }); @@ -58,30 +58,44 @@ module.exports.promiseSetupDriver = async() => { .forBrowser("firefox") .setFirefoxOptions(options); - const binaryLocation = await promiseActualBinary(process.env.FIREFOX_BINARY || "firefox"); + const binaryLocation = await promiseActualBinary( + process.env.FIREFOX_BINARY || "firefox", + ); await options.setBinary(new firefox.Binary(binaryLocation)); const driver = await builder.build(); driver.setContext(Context.CHROME); return driver; }; -module.exports.installAddon = async(driver) => { +module.exports.installAddon = async driver => { // references: // https://bugzilla.mozilla.org/show_bug.cgi?id=1298025 // https://github.com/mozilla/geckodriver/releases/tag/v0.17.0 const fileLocation = path.join(process.cwd(), process.env.XPI_NAME); const executor = driver.getExecutor(); - executor.defineCommand("installAddon", "POST", "/session/:sessionId/moz/addon/install"); + executor.defineCommand( + "installAddon", + "POST", + "/session/:sessionId/moz/addon/install", + ); const installCmd = new cmd.Command("installAddon"); const session = await driver.getSession(); - installCmd.setParameters({ sessionId: session.getId(), path: fileLocation, temporary: true }); + installCmd.setParameters({ + sessionId: session.getId(), + path: fileLocation, + temporary: true, + }); return executor.execute(installCmd); }; -module.exports.uninstallAddon = async(driver, id) => { +module.exports.uninstallAddon = async (driver, id) => { const executor = driver.getExecutor(); - executor.defineCommand("uninstallAddon", "POST", "/session/:sessionId/moz/addon/uninstall"); + executor.defineCommand( + "uninstallAddon", + "POST", + "/session/:sessionId/moz/addon/uninstall", + ); const uninstallCmd = new cmd.Command("uninstallAddon"); const session = await driver.getSession(); diff --git a/webpack.config.js b/webpack.config.js index c888164..ccc134a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,11 +1,11 @@ -var path = require('path'); -const webpack = require('webpack'); //to access built-in plugins +var path = require("path"); +const webpack = require("webpack"); //to access built-in plugins module.exports = { - entry: './src/StudyUtils.in.jsm', + entry: "./src/StudyUtils.in.jsm", output: { - path: path.resolve(__dirname, 'dist'), - filename: 'StudyUtils.jsm', - libraryTarget: 'this' // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var - } + path: path.resolve(__dirname, "dist"), + filename: "StudyUtils.jsm", + libraryTarget: "this", // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var + }, };