diff --git a/.circleci/config.yml b/.circleci/config.yml index e4296e1..00c1edf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,10 +64,6 @@ jobs: path: "test-addon/dist" destination: "test-addon/dist" - - run: - name: Import and build the Pioneer opt-in add-on - command: npm run import-pioneer-opt-in - # Needs signed add-on to work on branded releases #- run: # name: Test with Firefox Release diff --git a/bin/import-pioneer-opt-in.sh b/bin/import-pioneer-opt-in.sh deleted file mode 100755 index cdf3e8b..0000000 --- a/bin/import-pioneer-opt-in.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -echo "$@" - -set -eu -#set -o xtrace - -BASE_DIR="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" - -# download and build xpi for https://github.com/mozilla/pioneer-opt-in.git -if [ ! -d "pioneer-opt-in" ]; then - git clone https://github.com/mozilla/pioneer-opt-in.git -fi -cd pioneer-opt-in -bin/make-xpi.sh . -cd - - -echo -echo "SUCCESS: pioneer-opt-in xpi available at pioneer-opt-in/pioneer-opt-in.xpi" -echo diff --git a/examples/small-study/src/.eslintrc.js b/examples/small-study/src/.eslintrc.js index 8ef4351..7e9f4bb 100644 --- a/examples/small-study/src/.eslintrc.js +++ b/examples/small-study/src/.eslintrc.js @@ -7,7 +7,4 @@ module.exports = { es6: true, webextensions: true, }, - rules: { - "no-console": "off", - }, }; diff --git a/examples/small-study/src/studySetup.js b/examples/small-study/src/studySetup.js index 9b6385f..4d8a125 100644 --- a/examples/small-study/src/studySetup.js +++ b/examples/small-study/src/studySetup.js @@ -22,17 +22,15 @@ const baseStudySetup = { // used for activeExperiments tagging (telemetryEnvironment.setActiveExperiment) activeExperimentName: browser.runtime.id, - // use either "shield" or "pioneer" telemetry semantics and data pipelines + // uses shield sampling and telemetry semantics. Future: will support "pioneer" studyType: "shield", // telemetry telemetry: { - // Actually submit the pings to Telemetry. [default if omitted: false] + // default false. Actually send pings. send: true, - // Marks pings with testing=true. Set flag to `true` for pings are meant to be seen by analysts [default if omitted: false] + // Marks pings with testing=true. Set flag to `true` before final release removeTestingFlag: false, - // Keep an internal telemetry archive. Useful for verifying payloads of Pioneer studies without risking actually sending any unencrypted payloads [default if omitted: false] - internalTelemetryArchive: false, }, // endings with urls @@ -101,11 +99,10 @@ const baseStudySetup = { * * This implementation caches in local storage to speed up second run. * - * @param {object} studySetup A complete study setup object * @returns {Promise} answer An boolean answer about whether the user should be * allowed to enroll in the study */ -async function cachingFirstRunShouldAllowEnroll(studySetup) { +async function cachingFirstRunShouldAllowEnroll() { // Cached answer. Used on 2nd run let allowed = await browser.storage.local.get("allowedEnrollOnFirstRun"); if (allowed.allowedEnrollOnFirstRun === true) return true; @@ -116,13 +113,7 @@ async function cachingFirstRunShouldAllowEnroll(studySetup) { */ // could have other reasons to be eligible, such add-ons, prefs - const dataPermissions = await browser.study.getDataPermissions(); - if (studySetup.studyType === "shield") { - allowed = dataPermissions.shield; - } - if (studySetup.studyType === "pioneer") { - allowed = dataPermissions.pioneer; - } + allowed = true; // cache the answer await browser.storage.local.set({ allowedEnrollOnFirstRun: allowed }); @@ -138,7 +129,7 @@ async function getStudySetup() { // shallow copy const studySetup = Object.assign({}, baseStudySetup); - studySetup.allowEnroll = await cachingFirstRunShouldAllowEnroll(studySetup); + studySetup.allowEnroll = await cachingFirstRunShouldAllowEnroll(); const testingOverrides = await browser.study.getTestingOverrides(); studySetup.testing = { @@ -146,6 +137,5 @@ async function getStudySetup() { firstRunTimestamp: testingOverrides.firstRunTimestamp, expired: testingOverrides.expired, }; - return studySetup; } diff --git a/package-lock.json b/package-lock.json index c928fb6..85d8043 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5484,11 +5484,6 @@ "integrity": "sha1-LPn7rkbYB0/Ba33gBxyO/rykc6Y=", "dev": true }, - "jose-jwe-jws": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/jose-jwe-jws/-/jose-jwe-jws-0.1.6.tgz", - "integrity": "sha512-sZgrf5u15NmfiRAy3TAzQkIDsbXJ4zE11/rRTbQlGNFFy57u3B98U9JwH864+WfWM8BuJPqyCDDfDJJXUdWhiw==" - }, "js-select": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/js-select/-/js-select-0.6.0.tgz", diff --git a/package.json b/package.json index 00f2f57..f533484 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,7 @@ "version": "5.1.1", "author": "Mozilla", "bin": { - "copyStudyUtils": "bin/copyStudyUtils.js", - "importPioneerOptIn": "bin/import-pioneer-opt-in.sh" + "copyStudyUtils": "bin/copyStudyUtils.js" }, "bugs": { "url": "https://github.com/mozilla/shield-studies-addon-utils/issues" @@ -14,7 +13,6 @@ "ajv": "^6.5.0", "commander": "^2.15.1", "fs-extra": "^6.0.1", - "jose-jwe-jws": "0.1.6", "shield-study-schemas": "^0.8.3" }, "devDependencies": { @@ -43,7 +41,6 @@ }, "files": [ "bin/copyStudyUtils.js", - "bin/import-pioneer-opt-in.sh", "testUtils", "webExtensionApis/study/api.js", "webExtensionApis/study/schema.json", @@ -83,7 +80,6 @@ "generate:generateSchema:study": "cd webExtensionApis/study && yaml2json schema.yaml -p > schema.json", "generate:generateStubApi:study": "cd webExtensionApis/study && generateStubApi ./schema.json > stubApi.js", "generate:verifyWeeSchema:study": "cd webExtensionApis/study && verifyWeeSchema schema.json", - "import-pioneer-opt-in": "bin/import-pioneer-opt-in.sh", "lint": "npm-run-all lint:*", "lint:eslint": "npm run eslint", "lint:fixpack": "fixpack # cleans up package.json", @@ -91,7 +87,7 @@ "postformat": "run-p lint:fixpack eslint-fix", "prebuild": "if [ -z ${SKIPLINT} ]; then npm run lint; fi", "prepare": "export SKIPLINT=1 && fixpack && npm run build", - "pretest": "npm run build && npm run test-addon:bundle-utils && npm run test-addon:build && npm run import-pioneer-opt-in", + "pretest": "npm run build && npm run test-addon:bundle-utils && npm run test-addon:build", "pretest-addon": "npm run pretest", "small-study": "cd examples/small-study && npm run rebuild && npm start", "test": "npm run test:func", diff --git a/test-addon/src/studySetup.js b/test-addon/src/studySetup.js index 3a6ba9b..4d8a125 100644 --- a/test-addon/src/studySetup.js +++ b/test-addon/src/studySetup.js @@ -22,17 +22,15 @@ const baseStudySetup = { // used for activeExperiments tagging (telemetryEnvironment.setActiveExperiment) activeExperimentName: browser.runtime.id, - // use either "shield" or "pioneer" telemetry semantics and data pipelines - studyType: null, // set by internal test override below in getStudySetup() + // uses shield sampling and telemetry semantics. Future: will support "pioneer" + studyType: "shield", // telemetry telemetry: { - // Actually submit the pings to Telemetry. [default if omitted: false] + // default false. Actually send pings. send: true, - // Marks pings with testing=true. Set flag to `true` for pings are meant to be seen by analysts [default if omitted: false] + // Marks pings with testing=true. Set flag to `true` before final release removeTestingFlag: false, - // Keep an internal telemetry archive. Useful for verifying payloads of Pioneer studies without risking actually sending any unencrypted payloads [default if omitted: false] - internalTelemetryArchive: true, }, // endings with urls @@ -101,11 +99,10 @@ const baseStudySetup = { * * This implementation caches in local storage to speed up second run. * - * @param {object} studySetup A complete study setup object * @returns {Promise} answer An boolean answer about whether the user should be * allowed to enroll in the study */ -async function cachingFirstRunShouldAllowEnroll(studySetup) { +async function cachingFirstRunShouldAllowEnroll() { // Cached answer. Used on 2nd run let allowed = await browser.storage.local.get("allowedEnrollOnFirstRun"); if (allowed.allowedEnrollOnFirstRun === true) return true; @@ -116,13 +113,7 @@ async function cachingFirstRunShouldAllowEnroll(studySetup) { */ // could have other reasons to be eligible, such add-ons, prefs - const dataPermissions = await browser.study.getDataPermissions(); - if (studySetup.studyType === "shield") { - allowed = dataPermissions.shield; - } - if (studySetup.studyType === "pioneer") { - allowed = dataPermissions.pioneer; - } + allowed = true; // cache the answer await browser.storage.local.set({ allowedEnrollOnFirstRun: allowed }); @@ -138,11 +129,7 @@ async function getStudySetup() { // shallow copy const studySetup = Object.assign({}, baseStudySetup); - // internal testing override necessary to be able to test all study types - const internalTestingOverrides = await browser.studyDebug.getInternalTestingOverrides(); - studySetup.studyType = internalTestingOverrides.studyType; - - studySetup.allowEnroll = await cachingFirstRunShouldAllowEnroll(studySetup); + studySetup.allowEnroll = await cachingFirstRunShouldAllowEnroll(); const testingOverrides = await browser.study.getTestingOverrides(); studySetup.testing = { @@ -150,6 +137,5 @@ async function getStudySetup() { firstRunTimestamp: testingOverrides.firstRunTimestamp, expired: testingOverrides.expired, }; - return studySetup; } diff --git a/test/functional/browser.study.api.js b/test/functional/browser.study.api.js index b8246ec..ccb1d78 100644 --- a/test/functional/browser.study.api.js +++ b/test/functional/browser.study.api.js @@ -4,7 +4,7 @@ const KEEPOPEN = process.env.KEEPOPEN; /** Complete list of tests for testing * - * - the public api for `browser.study` not specific to any add-on background logic + * - the public api for `browser.study` */ /** About webdriver extension based tests @@ -43,7 +43,6 @@ const MINUTES_PER_DAY = 60 * 24; // node's util, for printing a deeply nested object to node console const { inspect } = require("util"); - // eslint-disable-next-line no-unused-vars function full(myObject) { return inspect(myObject, { showHidden: false, depth: null }); @@ -57,58 +56,55 @@ function merge(...sources) { return Object.assign({}, ...sources); } -function publicApiTests(studyType) { - /** return a studySetup, shallow merged from overrides - * - * @return {object} mergedStudySetup - */ - function studySetupForTests(...overrides) { - // Minimal configuration to pass schema validation - const studySetup = { - activeExperimentName: `shield-utils-test-addon@${studyType}.mozilla.org`, - studyType, - endings: { - ineligible: { - baseUrls: [ - "https://qsurvey.mozilla.com/s3/Shield-Study-Example-Survey/?reason=ineligible", - ], - }, - BrowserStudyApiEnding: { - baseUrls: [ - "https://qsurvey.mozilla.com/s3/Shield-Study-Example-Survey/?reason=BrowserStudyApiEnding", - ], - }, +/** return a studySetup, shallow merged from overrides + * + * @return {object} mergedStudySetup + */ +function studySetupForTests(...overrides) { + // Minimal configuration to pass schema validation + const studySetup = { + activeExperimentName: "shield-utils-test-addon@shield.mozilla.org", + studyType: "shield", + endings: { + ineligible: { + baseUrls: [ + "https://qsurvey.mozilla.com/s3/Shield-Study-Example-Survey/?reason=ineligible", + ], }, - telemetry: { - send: false, - removeTestingFlag: false, - internalTelemetryArchive: true, + BrowserStudyApiEnding: { + baseUrls: [ + "https://qsurvey.mozilla.com/s3/Shield-Study-Example-Survey/?reason=BrowserStudyApiEnding", + ], }, - logLevel: 10, - weightedVariations: [ - { - name: "control", - weight: 1, - }, - ], - expire: { - days: 14, + }, + telemetry: { + send: false, // assumed false. Actually send pings if true + removeTestingFlag: false, // Marks pings to be discarded, set true for to have the pings processed in the pipeline + }, + logLevel: 10, + weightedVariations: [ + { + name: "control", + weight: 1, }, - // Dynamic study configuration flags - allowEnroll: true, - testing: {}, - }; - - return merge(studySetup, ...overrides); - } + ], + expire: { + days: 14, + }, + // Dynamic study configuration flags + allowEnroll: true, + testing: {}, + }; + + return merge(studySetup, ...overrides); +} +describe("PUBLIC API `browser.study` (not specific to any add-on background logic)", function() { // This gives Firefox time to start, and us a bit longer during some of the tests. - this.timeout(30000 + KEEPOPEN * 1000 * 2); + this.timeout(15000 + KEEPOPEN * 1000 * 2); let driver; - let beginTime; let addonId; - // run in the extension page let addonExec; @@ -116,7 +112,7 @@ function publicApiTests(studyType) { driver = await utils.setupWebdriver.promiseSetupDriver( utils.FIREFOX_PREFERENCES, ); - await installAddon(); + addonId = await utils.setupWebdriver.installAddon(driver); await utils.ui.openBrowserConsole(driver); // make a shorter alias @@ -126,16 +122,9 @@ function publicApiTests(studyType) { ); } - async function installAddon() { - beginTime = Date.now(); - if (addonId) { - await utils.setupWebdriver.uninstallAddon(driver, addonId); - addonId = null; - } - if (studyType === "pioneer") { - await utils.setupWebdriver.installPioneerOptInAddon(driver); - } - addonId = await utils.setupWebdriver.installAddon(driver); + async function reinstallAddon() { + await utils.setupWebdriver.uninstallAddon(driver, addonId); + await utils.setupWebdriver.installAddon(driver); } before(createAddonExec); @@ -251,33 +240,6 @@ function publicApiTests(studyType) { }); }); - describe("getDataPermissions", function() { - it("returns correct and current list of permissions", async () => { - const thisSetup = studySetupForTests(); - const dataPermissions = await addonExec(async (setup, cb) => { - // this is what runs in the webExtension scope. - const $dataPermissions = await browser.study.getDataPermissions(); - // call back with all the data we care about to Mocha / node - cb($dataPermissions); - }, thisSetup); - // console.debug(full(dataPermissions)); - - // tests - assert(dataPermissions.shield, "shield should be enabled"); - if (studyType === "pioneer") { - assert( - dataPermissions.pioneer, - "user should have opted in for pioneer", - ); - } else { - assert( - !dataPermissions.pioneer, - "user should not have opted in for pioneer", - ); - } - }); - }); - describe("test the setup requirement", function() { it("should not be able to send telemetry before setup", async () => { const caughtError = await utils.executeJs.executeAsyncScriptInExtensionPageForTests( @@ -353,9 +315,9 @@ function publicApiTests(studyType) { // tests const now = Number(Date.now()); - const seenTelemetryStates = internals.seenTelemetry - .filter(ping => ping.payload.type === "shield-study") - .map(ping => ping.payload.data.study_state); + const seenTelemetryStates = internals.seenTelemetry["shield-study"].map( + x => x.data.study_state, + ); assert(internals.isSetup, "should be isSetup"); assert(!internals.isEnded, "should not be ended"); assert(!internals.isEnding, "should not be ending"); @@ -404,9 +366,9 @@ function publicApiTests(studyType) { const { info, internals } = data; // tests - const seenTelemetryStates = internals.seenTelemetry - .filter(ping => ping.payload.type === "shield-study") - .map(ping => ping.payload.data.study_state); + const seenTelemetryStates = internals.seenTelemetry["shield-study"].map( + x => x.data.study_state, + ); assert(internals.isSetup, "should be isSetup"); assert(!internals.isEnded, "should not be ended"); @@ -457,9 +419,9 @@ function publicApiTests(studyType) { const { info, internals } = data; // tests - const seenTelemetryStates = internals.seenTelemetry - .filter(ping => ping.payload.type === "shield-study") - .map(ping => ping.payload.data.study_state); + const seenTelemetryStates = internals.seenTelemetry["shield-study"].map( + x => x.data.study_state, + ); assert(internals.isSetup, "should be isSetup"); assert(internals.isEnded, "should be ended"); @@ -610,13 +572,12 @@ function publicApiTests(studyType) { describe("life-cycle tests", function() { describe("setup, sendTelemetry, manually invoked endStudy", function() { - let studyInfo, calculatedPingSize; + let studyInfo; const overrides = { activeExperimentName: "test:browser.study.api", telemetry: { send: true, removeTestingFlag: false, - internalTelemetryArchive: true, }, endings: { customEnding: { @@ -629,24 +590,15 @@ function publicApiTests(studyType) { }; before(async function reinstallSetupDoTelemetryAndWait() { - await installAddon(); - const _ = await addonExec(async (_studySetupForTests, callback) => { + await reinstallAddon(); + studyInfo = await addonExec(async (_studySetupForTests, callback) => { // Ensure we have a configured study and are supposed to run our feature browser.study.onReady.addListener(async _studyInfo => { - const samplePing = { foo: "bar" }; - await browser.study.sendTelemetry(samplePing); - const _calculatedPingSize = await browser.study.calculateTelemetryPingSize( - samplePing, - ); - callback({ - studyInfo: _studyInfo, - calculatedPingSize: _calculatedPingSize, - }); + await browser.study.sendTelemetry({ foo: "bar" }); + callback(_studyInfo); }); await browser.study.setup(_studySetupForTests); }, studySetupForTests(overrides)); - studyInfo = _.studyInfo; - calculatedPingSize = _.calculatedPingSize; await delay(1000); // wait a second to telemetry to settle on disk. }); @@ -658,35 +610,18 @@ function publicApiTests(studyType) { ); }); - it("calculated ping size is as expected", async () => { - const expectedPingSizes = { - shield: 20, - pioneer: 662, - }; - assert.strictEqual(calculatedPingSize, expectedPingSizes[studyType]); - }); - describe("telemetry archive / controller effects", function() { let studyPings; before(async () => { studyPings = await addonExec(async callback => { const _studyPings = await browser.study.searchSentTelemetry({ - type: [ - "shield-study", - "shield-study-addon", - "shield-study-error", - "pioneer-study", - ], + type: ["shield-study", "shield-study-addon"], }); - const internals = await browser.studyDebug.getInternals(); - callback({ - sent: _studyPings, - seen: internals.seenTelemetry.reverse(), - }); // Using reverse() to mimic the default sorting of telemetry archive results + callback(_studyPings); }); + // console.debug(full(studyPings.map(x => x.payload))); // For debugging tests - // console.debug("Pings report: ", utils.telemetry.pingsReport(studyPings.seen)); - // console.debug("Pings with id and payload: ", utils.telemetry.pingsDebug(studyPings.seen)); + // console.debug("Pings report: ", utils.telemetry.pingsReport(studyPings)); }); it("should have set the experiment to active in Telemetry", async () => { @@ -699,62 +634,67 @@ function publicApiTests(studyType) { ); }); + it("shield-study-addon telemetry should be working (as seen by telemetry)", async () => { + const shieldTelemetryPings = await addonExec(async callback => { + const _studyPings = await browser.study.searchSentTelemetry({ + type: ["shield-study-addon"], + }); + callback(_studyPings.map(x => x.payload)); + }); + // console.debug("pings", full(shieldTelemetryPings)); + assert(shieldTelemetryPings[0].data.attributes.foo === "bar"); + }); + it("should have sent at least one shield telemetry ping", async () => { + assert(studyPings.length > 0, "at least one shield telemetry ping"); + }); + + it("should have sent one shield-study telemetry ping with study_state=enter", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "enter", + ); assert( - studyPings.sent.length > 0, - "at least one shield telemetry ping", + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=enter", ); }); - it("should have sent expected telemetry", async () => { - const observed = utils.telemetry.summarizePings( - studyType === "shield" ? studyPings.sent : studyPings.seen, + it("should have sent one shield-study telemetry ping with study_state=installed", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "installed", ); - const expected = [ - [ - "shield-study-addon", - { - attributes: { - foo: "bar", - }, - }, - ], - [ - "shield-study", - { - study_state: "installed", - }, - ], - [ - "shield-study", - { - study_state: "enter", - }, - ], - ]; - assert.deepStrictEqual( - expected, - observed, - "telemetry pings as as expected", + assert( + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=installed", + ); + }); + + it("should have sent one shield-study-addon telemetry ping with payload.data.attributes.foo=bar", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study-addon" && + ping.payload.data.attributes.foo === "bar", + ); + assert( + filteredPings.length > 0, + "at least one shield-study-addon telemetry ping with payload.data.attributes.foo=bar", ); }); }); describe("browser.study.endStudy() side effects for first time called", function() { - let endingResult, endingInternals; + let endingResult; before(async () => { - const _ = await addonExec(async callback => { + endingResult = await addonExec(async callback => { browser.study.onEndStudy.addListener(async _endingResult => { - const internals = await browser.studyDebug.getInternals(); - callback({ - endingResult: _endingResult, - endingInternals: internals, - }); + callback(_endingResult); }); await browser.study.endStudy("customEnding"); }); - endingResult = _.endingResult; - endingInternals = _.endingInternals; // let telemetry and disk/files sync up await delay(1000); }); @@ -802,75 +742,36 @@ function publicApiTests(studyType) { let studyPings; before(async () => { - studyPings = {}; - studyPings.seen = endingInternals.seenTelemetry.reverse(); - studyPings.sent = await utils.telemetry.searchSentTelemetry( - driver, - { - type: [ - "shield-study", - "shield-study-addon", - "shield-study-error", - "pioneer-study", - ], - timestamp: beginTime, - }, - ); + studyPings = await utils.telemetry.searchSentTelemetry(driver, { + type: ["shield-study", "shield-study-addon"], + }); // For debugging tests - // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings.seen)); - // console.debug("Final pings with id and payload: ", utils.telemetry.pingsDebug(studyPings.seen)); + // console.debug(full(studyPings.map(x => [x.type, x.payload]))); + // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings)); }); - it("should have sent at least one shield telemetry ping", async () => { + it("one shield-study telemetry ping with study_state=exit", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "exit", + ); assert( - studyPings.sent.length > 0, - "at least one shield telemetry ping", + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=exit", ); }); - it("should have sent expected telemetry", async () => { - const observed = utils.telemetry.summarizePings( - studyType === "shield" ? studyPings.sent : studyPings.seen, + it("one shield-study telemetry ping with study_state_fullname=customEnding", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "ended-positive" && + ping.payload.data.study_state_fullname === "customEnding", ); - const expected = [ - [ - "shield-study", - { - study_state: "exit", - }, - ], - [ - "shield-study", - { - study_state: "ended-positive", - study_state_fullname: "customEnding", - }, - ], - [ - "shield-study-addon", - { - attributes: { - foo: "bar", - }, - }, - ], - [ - "shield-study", - { - study_state: "installed", - }, - ], - [ - "shield-study", - { - study_state: "enter", - }, - ], - ]; - assert.deepStrictEqual( - expected, - observed, - "telemetry pings as as expected", + assert( + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state_fullname=customEnding", ); }); }); @@ -878,13 +779,12 @@ function publicApiTests(studyType) { }); describe("setup of an ineligible study should result in endStudy('ineligible') without even emitting onReady", function() { - let endingResult, endingInternals; + let endingResult; const overrides = { activeExperimentName: "test:browser.study.api", telemetry: { send: true, removeTestingFlag: false, - internalTelemetryArchive: true, }, endings: { ineligible: { @@ -896,31 +796,31 @@ function publicApiTests(studyType) { }; before(async function reinstallSetupAndAwaitEndStudy() { - await installAddon(); - const _ = await addonExec(async (_studySetupForTests, callback) => { - // Ensure we have a configured study and are supposed to run our feature - browser.study.onEndStudy.addListener(async _endingResult => { - console.log( - "In resetSetupAndAwaitEndStudy - onEndStudy listener", - _endingResult, - ); - const internals = await browser.studyDebug.getInternals(); - callback({ - endingResult: _endingResult, - endingInternals: internals, + await reinstallAddon(); + endingResult = await addonExec( + async (_studySetupForTests, callback) => { + // Ensure we have a configured study and are supposed to run our feature + browser.study.onEndStudy.addListener(async _endingResult => { + console.log( + "In resetSetupAndAwaitEndStudy - onEndStudy listener", + _endingResult, + ); + callback(_endingResult); }); - }); - browser.study.onReady.addListener(async _studyInfo => { - console.log( - "In resetSetupAndAwaitEndStudy - onReady listener", - _studyInfo, - ); - throw new Error("onReady should not have been emitted", _studyInfo); - }); - await browser.study.setup(_studySetupForTests); - }, studySetupForTests(overrides)); - endingResult = _.endingResult; - endingInternals = _.endingInternals; + browser.study.onReady.addListener(async _studyInfo => { + console.log( + "In resetSetupAndAwaitEndStudy - onReady listener", + _studyInfo, + ); + throw new Error( + "onReady should not have been emitted", + _studyInfo, + ); + }); + await browser.study.setup(_studySetupForTests); + }, + studySetupForTests(overrides), + ); }); describe("browser.study.endStudy() side effects", function() { @@ -950,61 +850,35 @@ function publicApiTests(studyType) { let studyPings; before(async () => { - studyPings = {}; - studyPings.seen = endingInternals.seenTelemetry.reverse(); - studyPings.sent = await utils.telemetry.searchSentTelemetry( - driver, - { - type: [ - "shield-study", - "shield-study-addon", - "shield-study-error", - "pioneer-study", - ], - timestamp: beginTime, - }, - ); + studyPings = await utils.telemetry.searchSentTelemetry(driver, { + type: ["shield-study", "shield-study-addon"], + }); // For debugging tests - // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings.seen)); - // console.debug("Final pings with id and payload: ", utils.telemetry.pingsDebug(studyPings.seen)); + // console.debug(full(studyPings.map(x => [x.type, x.payload]))); + // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings)); }); - it("should have sent at least one shield telemetry ping", async () => { + it("one shield-study telemetry ping with study_state=exit", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "exit", + ); assert( - studyPings.sent.length > 0, - "at least one shield telemetry ping", + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=exit", ); }); - it("should have sent expected telemetry", async () => { - const observed = utils.telemetry.summarizePings( - studyType === "shield" ? studyPings.sent : studyPings.seen, + it("one shield-study telemetry ping with study_state=ineligible", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "ineligible", ); - const expected = [ - [ - "shield-study", - { - study_state: "exit", - }, - ], - [ - "shield-study", - { - study_state: "ineligible", - study_state_fullname: "ineligible", - }, - ], - [ - "shield-study", - { - study_state: "enter", - }, - ], - ]; - assert.deepStrictEqual( - expected, - observed, - "telemetry pings as as expected", + assert( + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=ineligible", ); }); }); @@ -1012,13 +886,12 @@ function publicApiTests(studyType) { }); describe("setup of an already expired study should result in endStudy('expired') without even emitting onReady", function() { - let endingResult, endingInternals; + let endingResult; const overrides = { activeExperimentName: "test:browser.study.api", telemetry: { send: true, removeTestingFlag: false, - internalTelemetryArchive: true, }, endings: { expired: { @@ -1033,31 +906,31 @@ function publicApiTests(studyType) { }; before(async function reinstallSetupAndAwaitEndStudy() { - await installAddon(); - const _ = await addonExec(async (_studySetupForTests, callback) => { - // Ensure we have a configured study and are supposed to run our feature - browser.study.onEndStudy.addListener(async _endingResult => { - console.log( - "In resetSetupAndAwaitEndStudy - onEndStudy listener", - _endingResult, - ); - const internals = await browser.studyDebug.getInternals(); - callback({ - endingResult: _endingResult, - endingInternals: internals, + await reinstallAddon(); + endingResult = await addonExec( + async (_studySetupForTests, callback) => { + // Ensure we have a configured study and are supposed to run our feature + browser.study.onEndStudy.addListener(async _endingResult => { + console.log( + "In resetSetupAndAwaitEndStudy - onEndStudy listener", + _endingResult, + ); + callback(_endingResult); }); - }); - browser.study.onReady.addListener(async _studyInfo => { - console.log( - "In resetSetupAndAwaitEndStudy - onReady listener", - _studyInfo, - ); - throw new Error("onReady should not have been emitted", _studyInfo); - }); - await browser.study.setup(_studySetupForTests); - }, studySetupForTests(overrides)); - endingResult = _.endingResult; - endingInternals = _.endingInternals; + browser.study.onReady.addListener(async _studyInfo => { + console.log( + "In resetSetupAndAwaitEndStudy - onReady listener", + _studyInfo, + ); + throw new Error( + "onReady should not have been emitted", + _studyInfo, + ); + }); + await browser.study.setup(_studySetupForTests); + }, + studySetupForTests(overrides), + ); }); describe("browser.study.endStudy() side effects", function() { @@ -1087,61 +960,35 @@ function publicApiTests(studyType) { let studyPings; before(async () => { - studyPings = {}; - studyPings.seen = endingInternals.seenTelemetry.reverse(); - studyPings.sent = await utils.telemetry.searchSentTelemetry( - driver, - { - type: [ - "shield-study", - "shield-study-addon", - "shield-study-error", - "pioneer-study", - ], - timestamp: beginTime, - }, - ); + studyPings = await utils.telemetry.searchSentTelemetry(driver, { + type: ["shield-study", "shield-study-addon"], + }); // For debugging tests - // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings.seen)); - // console.debug("Final pings with id and payload: ", utils.telemetry.pingsDebug(studyPings.seen)); + // console.debug(full(studyPings.map(x => [x.type, x.payload]))); + // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings)); }); - it("should have sent at least one shield telemetry ping", async () => { + it("one shield-study telemetry ping with study_state=exit", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "exit", + ); assert( - studyPings.sent.length > 0, - "at least one shield telemetry ping", + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=exit", ); }); - it("should have sent expected telemetry", async () => { - const observed = utils.telemetry.summarizePings( - studyType === "shield" ? studyPings.sent : studyPings.seen, + it("one shield-study telemetry ping with study_state=expired", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "expired", ); - const expected = [ - [ - "shield-study", - { - study_state: "exit", - }, - ], - [ - "shield-study", - { - study_state: "expired", - study_state_fullname: "expired", - }, - ], - [ - "shield-study", - { - study_state: "enter", - }, - ], - ]; - assert.deepStrictEqual( - expected, - observed, - "telemetry pings as as expected", + assert( + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=expired", ); }); }); @@ -1149,13 +996,14 @@ function publicApiTests(studyType) { }); describe("setup of a study that expires within a few seconds should result in endStudy('expired') after a few seconds", function() { - let endingResult, endingInternals; + let endingResult; + const now = Number(Date.now()); + const msInOneDay = 60 * 60 * 24 * 1000; const overrides = { activeExperimentName: "test:browser.study.api", telemetry: { send: true, removeTestingFlag: false, - internalTelemetryArchive: true, }, expire: { days: 1, @@ -1168,65 +1016,56 @@ function publicApiTests(studyType) { }, }, testing: { - firstRunTimestamp: null, // needs to be set in the before-hook below in order to be executed just before the setup of the study + firstRunTimestamp: now - msInOneDay + 2000, }, }; before(async function reinstallSetupAndConfigureAlarm() { - await installAddon(); - // Set the study to expire after a few seconds - const now = Number(Date.now()); - const msInOneDay = 60 * 60 * 24 * 1000; - overrides.testing.firstRunTimestamp = now - msInOneDay + 5000; - // console.log("Expiration debug: now, firstRunTimestamp, new Date(), new Date(now), new Date(firstRunTimestamp)", now, overrides.testing.firstRunTimestamp, new Date(), new Date(now), new Date(overrides.testing.firstRunTimestamp)); - const _ = await addonExec(async (_studySetupForTests, callback) => { - console.log( - "In resetSetupAndConfigureAlarm - addonExec", - _studySetupForTests, - ); - // Ensure we have a configured study and are supposed to run our feature - browser.study.onEndStudy.addListener(async _endingResult => { + await reinstallAddon(); + endingResult = await addonExec( + async (_studySetupForTests, callback) => { console.log( - "In resetSetupAndConfigureAlarm - onEndStudy listener", - _endingResult, + "In resetSetupAndConfigureAlarm - addonExec", + _studySetupForTests, ); - const internals = await browser.studyDebug.getInternals(); - callback({ - endingResult: _endingResult, - endingInternals: internals, + // Ensure we have a configured study and are supposed to run our feature + browser.study.onEndStudy.addListener(async _endingResult => { + console.log( + "In resetSetupAndConfigureAlarm - onEndStudy listener", + _endingResult, + ); + callback(_endingResult); }); - }); - browser.study.onReady.addListener(async _studyInfo => { - console.log( - "In resetSetupAndConfigureAlarm - onReady listener", - _studyInfo, - ); - await browser.study.sendTelemetry({ foo: "bar" }); - const { delayInMinutes } = _studyInfo; - if (delayInMinutes !== undefined) { - const alarmName = `${browser.runtime.id}:studyExpiration`; - const alarmListener = async alarm => { - console.log( - "In resetSetupAndConfigureAlarm - alarmListener", - alarm, - ); - if (alarm.name === alarmName) { - browser.alarms.onAlarm.removeListener(alarmListener); - await browser.study.endStudy("expired"); - } - }; - console.log("Setting up alarm listener", alarmListener); - browser.alarms.onAlarm.addListener(alarmListener); - console.log("Creating alarm", alarmName, delayInMinutes); - browser.alarms.create(alarmName, { - delayInMinutes, - }); - } - }); - await browser.study.setup(_studySetupForTests); - }, studySetupForTests(overrides)); - endingResult = _.endingResult; - endingInternals = _.endingInternals; + browser.study.onReady.addListener(async _studyInfo => { + console.log( + "In resetSetupAndConfigureAlarm - onReady listener", + _studyInfo, + ); + const { delayInMinutes } = _studyInfo; + if (delayInMinutes !== undefined) { + const alarmName = `${browser.runtime.id}:studyExpiration`; + const alarmListener = async alarm => { + console.log( + "In resetSetupAndConfigureAlarm - alarmListener", + alarm, + ); + if (alarm.name === alarmName) { + browser.alarms.onAlarm.removeListener(alarmListener); + await browser.study.endStudy("expired"); + } + }; + console.log("Setting up alarm listener", alarmListener); + browser.alarms.onAlarm.addListener(alarmListener); + console.log("Creating alarm", alarmName, delayInMinutes); + browser.alarms.create(alarmName, { + delayInMinutes, + }); + } + }); + await browser.study.setup(_studySetupForTests); + }, + studySetupForTests(overrides), + ); }); describe("browser.study.endStudy() side effects", function() { @@ -1256,63 +1095,35 @@ function publicApiTests(studyType) { let studyPings; before(async () => { - studyPings = {}; - studyPings.seen = endingInternals.seenTelemetry.reverse(); - studyPings.sent = await utils.telemetry.searchSentTelemetry( - driver, - { - type: [ - "shield-study", - "shield-study-addon", - "shield-study-error", - "pioneer-study", - ], - timestamp: beginTime, - }, - ); + studyPings = await utils.telemetry.searchSentTelemetry(driver, { + type: ["shield-study", "shield-study-addon"], + }); // For debugging tests - // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings.seen)); - // console.debug("Final pings with id and payload: ", utils.telemetry.pingsDebug(studyPings.seen)); + // console.debug(full(studyPings.map(x => [x.type, x.payload]))); + // console.debug("Final pings report: ", utils.telemetry.pingsReport(studyPings)); }); - it("should have sent at least one shield telemetry ping", async () => { + it("one shield-study telemetry ping with study_state=exit", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "exit", + ); assert( - studyPings.sent.length > 0, - "at least one shield telemetry ping", + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=exit", ); }); - it("should have sent expected telemetry", async () => { - const observed = utils.telemetry.summarizePings( - studyType === "shield" ? studyPings.sent : studyPings.seen, + it("one shield-study telemetry ping with study_state=expired", async () => { + const filteredPings = studyPings.filter( + ping => + ping.type === "shield-study" && + ping.payload.data.study_state === "expired", ); - const expected = [ - [ - "shield-study", - { - study_state: "exit", - }, - ], - [ - "shield-study", - { - study_state: "expired", - study_state_fullname: "expired", - }, - ], - [ - "shield-study-addon", - { - attributes: { - foo: "bar", - }, - }, - ], - ]; - assert.deepStrictEqual( - expected, - observed, - "telemetry pings as as expected", + assert( + filteredPings.length > 0, + "at least one shield-study telemetry ping with study_state=expired", ); }); }); @@ -1357,13 +1168,35 @@ function publicApiTests(studyType) { }); }); - // TODO 5.2+ - describe.skip("possible 5.2+ future tests.", function() { - describe("uninstall by users?", function() {}); + describe("endStudy", function() { + it("see the browser.study.endStudy() side effects above", () => + assert(true)); + }); + + describe("getStudyInfo", function() { + describe("correctness: see browser.study.setup() tests", function() { + // tests + it("during first run, isFirstRun is true", function() {}); + it("during second run, isFirstRun is false", function() {}); + it("if duration.days in studySetup(), have a delayInMinutes in studyInfo", async function() {}); + }); + }); + + describe("searchSentTelemetry (light testing)", function() { + it("searches get results, see the endStudy() and other test", function() {}); + }); + + describe("uninstall by users?", function() {}); + + // TODO 5.1 + describe.skip("possible 5.1 future tests.", function() { + describe("getDataPermissions", function() { + it("returns correct and current list of permissions"); + }); describe("surveyUrl", function() { describe("needs setup", function() { - it("throws StudyNotSetupError if not setup"); + it("throws StudyNotsSetupError if not setup"); }); describe("correctly constructs urls queryArgs from profile info", function() { it("an example url is correct"); @@ -1373,12 +1206,4 @@ function publicApiTests(studyType) { it("log level works?"); }); }); -} - -describe("PUBLIC API `browser.study` (studyType: shield)", function() { - publicApiTests.call(this, "shield"); -}); - -describe("PUBLIC API `browser.study` (studyType: pioneer)", function() { - publicApiTests.call(this, "pioneer"); }); diff --git a/test/functional/test-addon.js b/test/functional/test-addon.js index 020cd1c..6256024 100644 --- a/test/functional/test-addon.js +++ b/test/functional/test-addon.js @@ -12,7 +12,7 @@ const KEEPOPEN = process.env.KEEPOPEN; const assert = require("assert"); const utils = require("./utils"); -const testAddonTests = function(studyType) { +describe("Tests verifying that the test add-on works as expected", function() { // This gives Firefox time to start, and us a bit longer during some of the tests. this.timeout(15000 + KEEPOPEN * 1000 * 3); @@ -22,17 +22,6 @@ const testAddonTests = function(studyType) { driver = await utils.setupWebdriver.promiseSetupDriver( utils.FIREFOX_PREFERENCES, ); - const widgetId = utils.ui.makeWidgetId( - "shield-utils-test-addon@shield.mozilla.org", - ); - await utils.preferences.set( - driver, - `extensions.${widgetId}.test.studyType`, - studyType, - ); - if (studyType === "pioneer") { - await utils.setupWebdriver.installPioneerOptInAddon(driver); - } await utils.setupWebdriver.installAddon(driver); await utils.ui.openBrowserConsole(driver); }); @@ -158,12 +147,4 @@ const testAddonTests = function(studyType) { */ }); }); -}; - -describe("Tests verifying that the test add-on works as expected (studyType: shield)", function() { - testAddonTests.bind(this)("shield"); -}); - -describe("Tests verifying that the test add-on works as expected (studyType: pioneer)", function() { - testAddonTests.bind(this)("pioneer"); }); diff --git a/testUtils/setupWebdriver.js b/testUtils/setupWebdriver.js index b22e6a0..a7ad728 100644 --- a/testUtils/setupWebdriver.js +++ b/testUtils/setupWebdriver.js @@ -107,20 +107,6 @@ module.exports.setupWebdriver = { return addonId; }, - /** Install pioneer opt-in add-on from where it is expected to be if its - * repo is cloned in the current working directory and the xpi then built within - * - * @param {object} driver Configured Firefox webdriver - * @param {string} fileLocation location for add-on xpi/zip - * @returns {Promise} returns add-on id) - */ - installPioneerOptInAddon: async (driver, fileLocation) => { - fileLocation = - fileLocation || - path.join(process.cwd(), "pioneer-opt-in/pioneer-opt-in.xpi"); - return module.exports.setupWebdriver.installAddon(driver, fileLocation); - }, - uninstallAddon: async (driver, addonId) => { const executor = driver.getExecutor(); executor.defineCommand( diff --git a/testUtils/telemetry.js b/testUtils/telemetry.js index 7bb0eaa..974b6a7 100644 --- a/testUtils/telemetry.js +++ b/testUtils/telemetry.js @@ -7,14 +7,6 @@ const { const firefox = require("selenium-webdriver/firefox"); const Context = firefox.Context; -// node's util, for printing a deeply nested object to node console -const { inspect } = require("util"); - -// eslint-disable-next-line no-unused-vars -function full(myObject) { - return inspect(myObject, { showHidden: false, depth: null }); -} - module.exports.telemetry = { getActiveExperiments: async driver => { driver.setContext(Context.CHROME); @@ -60,14 +52,6 @@ module.exports.telemetry = { return pings.map(p => [p.payload.type, p.payload.data]); }, - pingsDebug: pings => { - return full( - pings.map(x => { - return { id: x.id, payload: x.payload }; - }), - ); - }, - pingsReport: pings => { if (pings.length === 0) { return { report: "No pings found" }; diff --git a/testUtils/ui.js b/testUtils/ui.js index 75827df..e1cda83 100644 --- a/testUtils/ui.js +++ b/testUtils/ui.js @@ -26,18 +26,6 @@ module.exports.ui = { return JSON.parse(manifestJson); }, - /** - * From firefox/browser/components/extensions/ExtensionPopups.jsm - * - * @param {string} id Id to modify - * @returns {string} widgetId canonical widget id with replaced bits. - */ - makeWidgetId: id => { - id = id.toLowerCase(); - // FIXME: This allows for collisions. - return id.replace(/[^a-z0-9_-]/g, "_"); - }, - /** * The widget id is used to identify add-on specific chrome elements. Examples: * - Browser action - {addonWidgetId}-browser-action @@ -46,8 +34,20 @@ module.exports.ui = { * @returns {Promise} name of the made widget */ addonWidgetId: async () => { + /** + * From firefox/browser/components/extensions/ExtensionPopups.jsm + * + * @param {string} id Id to modify + * @returns {string} widgetId canonical widget id with replaced bits. + */ + function makeWidgetId(id) { + id = id.toLowerCase(); + // FIXME: This allows for collisions. + return id.replace(/[^a-z0-9_-]/g, "_"); + } + const manifest = await module.exports.ui.promiseManifest(); - return module.exports.ui.makeWidgetId(manifest.applications.gecko.id); + return makeWidgetId(manifest.applications.gecko.id); }, openBrowserConsole: async driver => { diff --git a/webExtensionApis/study/api.md b/webExtensionApis/study/api.md index f00a366..7f59d2d 100644 --- a/webExtensionApis/study/api.md +++ b/webExtensionApis/study/api.md @@ -11,7 +11,7 @@ Attempt an setup/enrollment, with these effects: * sets 'studyType' as Shield or Pioneer * affects telemetry - * (5.2+ TODO) watches for dataPermission changes that should _always_ + * (5.1 TODO) watches for dataPermission changes that should _always_ stop that kind of study * Use or choose variation @@ -139,20 +139,18 @@ Throws Error if called before `browser.study.setup` **Parameters** -### `browser.study.getDataPermissions( )` - -Object of current dataPermissions (shield enabled true/false, pioneer enabled true/false) - -**Parameters** - ### `browser.study.sendTelemetry( payload )` Send Telemetry using appropriate shield or pioneer methods. -shield & pioneer sends using the following schema: +shield: * `shield-study-addon` ping, requires object string keys and string values +pioneer: + +* TBD + Note: * no conversions / coercion of data happens. @@ -160,30 +158,12 @@ Note: Note: * undefined what happens if validation fails +* undefined what happens when you try to send 'shield' from 'pioneer' TBD fix the parameters here. **Parameters** -* `payload` - * type: payload - * $ref: - * optional: false - -### `browser.study.calculateTelemetryPingSize( payload )` - -Calculate Telemetry using appropriate shield or pioneer methods. - -shield: - -* Calculate the size of a ping - -pioneer: - -* Calculate the size of a ping that has Pioneer encrypted data - -**Parameters** - * `payload` * type: payload * $ref: @@ -304,34 +284,7 @@ Act on it by } ``` -### [1] NullableBoolean - -```json -{ - "id": "NullableBoolean", - "$schema": "http://json-schema.org/draft-04/schema", - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ], - "choices": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ], - "testcases": [null, true, false], - "failcases": ["1234567890", "foo", []] -} -``` - -### [2] NullableInteger +### [1] NullableInteger ```json { @@ -358,7 +311,7 @@ Act on it by } ``` -### [3] NullableNumber +### [2] NullableNumber ```json { @@ -385,7 +338,7 @@ Act on it by } ``` -### [4] studyTypesEnum +### [3] studyTypesEnum ```json { @@ -398,7 +351,7 @@ Act on it by } ``` -### [5] weightedVariationObject +### [4] weightedVariationObject ```json { @@ -422,7 +375,7 @@ Act on it by } ``` -### [6] weightedVariationsArray +### [5] weightedVariationsArray ```json { @@ -455,7 +408,7 @@ Act on it by } ``` -### [7] anEndingRequest +### [6] anEndingRequest ```json { @@ -566,7 +519,7 @@ Act on it by } ``` -### [8] onEndStudyResponse +### [7] onEndStudyResponse ```json { @@ -588,7 +541,7 @@ Act on it by } ``` -### [9] studyInfoObject +### [8] studyInfoObject ```json { @@ -622,26 +575,7 @@ Act on it by } ``` -### [10] dataPermissionsObject - -```json -{ - "id": "dataPermissionsObject", - "type": "object", - "additionalProperties": false, - "properties": { - "shield": { - "type": "boolean" - }, - "pioneer": { - "type": "boolean" - } - }, - "required": ["shield", "pioneer"] -} -``` - -### [11] studySetup +### [9] studySetup ```json { @@ -682,10 +616,6 @@ Act on it by }, "removeTestingFlag": { "type": "boolean" - }, - "internalTelemetryArchive": { - "optional": true, - "$ref": "NullableBoolean" } } }, @@ -774,8 +704,7 @@ Act on it by ], "telemetry": { "send": false, - "removeTestingFlag": false, - "internalTelemetryArchive": false + "removeTestingFlag": false }, "testing": { "variationName": "something", @@ -799,8 +728,7 @@ Act on it by ], "telemetry": { "send": false, - "removeTestingFlag": true, - "internalTelemetryArchive": true + "removeTestingFlag": true }, "testing": { "variationName": "something", @@ -859,7 +787,7 @@ Act on it by } ``` -### [12] telemetryPayload +### [10] telemetryPayload ```json { @@ -873,7 +801,7 @@ Act on it by } ``` -### [13] searchTelemetryQuery +### [11] searchTelemetryQuery ```json { @@ -911,7 +839,7 @@ Act on it by } ``` -### [14] anEndingAnswer +### [12] anEndingAnswer ```json { @@ -1078,20 +1006,11 @@ About `this._internals`: * isSetup: bool `setup` * isFirstRun: bool `setup`, based on pref * studySetup: bool `setup` the config -* seenTelemetry: array of seen telemetry. Fully populated only if studySetup.telemetry.internalTelemetryArchive is true +* seenTelemetry: object of lists of seen telemetry by bucket * prefs: object of all created prefs and their names **Parameters** -### `browser.studyDebug.getInternalTestingOverrides( )` - -Returns an object with the following keys: -studyType - to be able to test add-ons with different studyType configurations -Used to override study testing flags in getStudySetup(). -The values are set by the corresponding preference under the `extensions.${widgetId}.test.*` preference branch. - -**Parameters** - ## Events (None) diff --git a/webExtensionApis/study/schema.json b/webExtensionApis/study/schema.json index 282e2ab..88f0737 100644 --- a/webExtensionApis/study/schema.json +++ b/webExtensionApis/study/schema.json @@ -25,28 +25,6 @@ ], "testcases": [null, "a string"] }, - { - "id": "NullableBoolean", - "$schema": "http://json-schema.org/draft-04/schema", - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ], - "choices": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ], - "testcases": [null, true, false], - "failcases": ["1234567890", "foo", []] - }, { "id": "NullableInteger", "$schema": "http://json-schema.org/draft-04/schema", @@ -298,20 +276,6 @@ "isFirstRun" ] }, - { - "id": "dataPermissionsObject", - "type": "object", - "additionalProperties": false, - "properties": { - "shield": { - "type": "boolean" - }, - "pioneer": { - "type": "boolean" - } - }, - "required": ["shield", "pioneer"] - }, { "id": "studySetup", "$schema": "http://json-schema.org/draft-04/schema", @@ -350,10 +314,6 @@ }, "removeTestingFlag": { "type": "boolean" - }, - "internalTelemetryArchive": { - "optional": true, - "$ref": "NullableBoolean" } } }, @@ -442,8 +402,7 @@ ], "telemetry": { "send": false, - "removeTestingFlag": false, - "internalTelemetryArchive": false + "removeTestingFlag": false }, "testing": { "variationName": "something", @@ -467,8 +426,7 @@ ], "telemetry": { "send": false, - "removeTestingFlag": true, - "internalTelemetryArchive": true + "removeTestingFlag": true }, "testing": { "variationName": "something", @@ -581,7 +539,7 @@ "type": "function", "async": true, "description": - "Attempt an setup/enrollment, with these effects:\n\n- sets 'studyType' as Shield or Pioneer\n - affects telemetry\n - (5.2+ TODO) watches for dataPermission changes that should *always*\n stop that kind of study\n\n- Use or choose variation\n - `testing.variation` if present\n - OR (internal) deterministicVariation\n from `weightedVariations`\n based on hash of\n\n - activeExperimentName\n - clientId\n\n- During firstRun[1] only:\n - set firstRunTimestamp pref value\n - send 'enter' ping\n - if `allowEnroll`, send 'install' ping\n - else endStudy(\"ineligible\") and return\n\n- Every Run\n - setActiveExperiment(studySetup)\n - monitor shield | pioneer permission endings\n - suggests alarming if `expire` is set.\n\nReturns:\n- studyInfo object (see `getStudyInfo`)\n\nTelemetry Sent (First run only)\n\n - enter\n - install\n\nFires Events\n\n(At most one of)\n- study:onReady OR\n- study:onEndStudy\n\nPreferences set\n- `shield.${runtime.id}.firstRunTimestamp`\n\nNote:\n1. allowEnroll is ONLY used during first run (install)\n", + "Attempt an setup/enrollment, with these effects:\n\n- sets 'studyType' as Shield or Pioneer\n - affects telemetry\n - (5.1 TODO) watches for dataPermission changes that should *always*\n stop that kind of study\n\n- Use or choose variation\n - `testing.variation` if present\n - OR (internal) deterministicVariation\n from `weightedVariations`\n based on hash of\n\n - activeExperimentName\n - clientId\n\n- During firstRun[1] only:\n - set firstRunTimestamp pref value\n - send 'enter' ping\n - if `allowEnroll`, send 'install' ping\n - else endStudy(\"ineligible\") and return\n\n- Every Run\n - setActiveExperiment(studySetup)\n - monitor shield | pioneer permission endings\n - suggests alarming if `expire` is set.\n\nReturns:\n- studyInfo object (see `getStudyInfo`)\n\nTelemetry Sent (First run only)\n\n - enter\n - install\n\nFires Events\n\n(At most one of)\n- study:onReady OR\n- study:onEndStudy\n\nPreferences set\n- `shield.${runtime.id}.firstRunTimestamp`\n\nNote:\n1. allowEnroll is ONLY used during first run (install)\n", "parameters": [ { "name": "studySetup", @@ -635,28 +593,11 @@ } ] }, - { - "name": "getDataPermissions", - "type": "function", - "async": true, - "description": - "Object of current dataPermissions (shield enabled true/false, pioneer enabled true/false)", - "defaultReturn": { - "shield": true, - "pioneer": false - }, - "parameters": [], - "returns": [ - { - "$ref": "dataPermissionsObject" - } - ] - }, { "name": "sendTelemetry", "type": "function", "description": - "Send Telemetry using appropriate shield or pioneer methods.\n\nshield & pioneer sends using the following schema:\n- `shield-study-addon` ping, requires object string keys and string values\n\nNote:\n- no conversions / coercion of data happens.\n\nNote:\n- undefined what happens if validation fails\n\nTBD fix the parameters here.\n", + "Send Telemetry using appropriate shield or pioneer methods.\n\nshield:\n- `shield-study-addon` ping, requires object string keys and string values\n\npioneer:\n- TBD\n\nNote:\n- no conversions / coercion of data happens.\n\nNote:\n- undefined what happens if validation fails\n- undefined what happens when you try to send 'shield' from 'pioneer'\n\nTBD fix the parameters here.\n", "async": true, "parameters": [ { @@ -667,25 +608,6 @@ "defaultReturn": "undefined", "returns": null }, - { - "name": "calculateTelemetryPingSize", - "type": "function", - "description": - "Calculate Telemetry using appropriate shield or pioneer methods.\n\nshield:\n- Calculate the size of a ping\n\npioneer:\n- Calculate the size of a ping that has Pioneer encrypted data\n", - "async": true, - "parameters": [ - { - "name": "payload", - "$ref": "telemetryPayload" - } - ], - "defaultReturn": "undefined", - "returns": [ - { - "type": "number" - } - ] - }, { "name": "searchSentTelemetry", "type": "function", @@ -942,15 +864,7 @@ "type": "function", "async": true, "description": - "Return `_internals` of the studyUtils object.\n\nUse this for debugging state.\n\nAbout `this._internals`:\n- variation: (chosen variation, `setup` )\n- isEnding: bool `endStudy`\n- isSetup: bool `setup`\n- isFirstRun: bool `setup`, based on pref\n- studySetup: bool `setup` the config\n- seenTelemetry: array of seen telemetry. Fully populated only if studySetup.telemetry.internalTelemetryArchive is true\n- prefs: object of all created prefs and their names\n", - "parameters": [] - }, - { - "name": "getInternalTestingOverrides", - "type": "function", - "async": true, - "description": - "Returns an object with the following keys:\n studyType - to be able to test add-ons with different studyType configurations\nUsed to override study testing flags in getStudySetup().\nThe values are set by the corresponding preference under the `extensions.${widgetId}.test.*` preference branch.\n", + "Return `_internals` of the studyUtils object.\n\nUse this for debugging state.\n\nAbout `this._internals`:\n- variation: (chosen variation, `setup` )\n- isEnding: bool `endStudy`\n- isSetup: bool `setup`\n- isFirstRun: bool `setup`, based on pref\n- studySetup: bool `setup` the config\n- seenTelemetry: object of lists of seen telemetry by bucket\n- prefs: object of all created prefs and their names\n", "parameters": [] } ] diff --git a/webExtensionApis/study/schema.yaml b/webExtensionApis/study/schema.yaml index 66b59e0..944e9a8 100644 --- a/webExtensionApis/study/schema.yaml +++ b/webExtensionApis/study/schema.yaml @@ -34,25 +34,14 @@ {type: 'string'}] testcases: [null, 'a string'] - - id: NullableBoolean - $schema: "http://json-schema.org/draft-04/schema" - oneOf: [ - {type: 'null'}, - {type: 'boolean'}] - choices: [ - {type: 'null'}, - {type: 'boolean'}] - testcases: [null, true, false] - failcases: ['1234567890', 'foo', []] - - id: NullableInteger $schema: "http://json-schema.org/draft-04/schema" oneOf: [ - {type: 'null'}, - {type: 'integer'}] + {type: 'null'}, + {type: 'integer'}] choices: [ - {type: 'null'}, - {type: 'integer'}] + {type: 'null'}, + {type: 'integer'}] testcases: [null, 1234567890] failcases: ['1234567890', []] @@ -180,20 +169,16 @@ - activeExperimentName - isFirstRun - - id: dataPermissionsObject - type: object - additionalProperties: false - properties: - shield: - type: - boolean - pioneer: - type: - boolean - - required: - - shield - - pioneer + #- id: dataPermissionsObject + # type: object + # additionalProperties: true + # properties: + # shield: + # type: + # boolean + # + # required: + # - shield - id: studySetup $schema: "http://json-schema.org/draft-04/schema" @@ -223,9 +208,6 @@ type: boolean removeTestingFlag: type: boolean - internalTelemetryArchive: - optional: true - $ref: NullableBoolean testing: type: object properties: @@ -264,7 +246,7 @@ expire: { "days": 10}, endings: {anEnding: {baseUrls: ['some.url']}}, weightedVariations: [{name: feature-active, weight: 1.5}], - telemetry: { send: false, removeTestingFlag: false, internalTelemetryArchive: false}, + telemetry: { send: false, removeTestingFlag: false}, testing: { variationName: "something", firstRunTimestamp: 1234567890, @@ -276,7 +258,7 @@ studyType: 'pioneer', endings: {anEnding: {baseUrls: ['some.url']}}, weightedVariations: [{name: feature-active, weight: 1.5}], - telemetry: { send: false, removeTestingFlag: true, internalTelemetryArchive: true}, + telemetry: { send: false, removeTestingFlag: true}, testing: { variationName: "something", firstRunTimestamp: 1234567890, @@ -317,7 +299,7 @@ - sets 'studyType' as Shield or Pioneer - affects telemetry - - (5.2+ TODO) watches for dataPermission changes that should *always* + - (5.1 TODO) watches for dataPermission changes that should *always* stop that kind of study - Use or choose variation @@ -441,14 +423,14 @@ returns: - $ref: studyInfoObject - - name: getDataPermissions - type: function - async: true - description: Object of current dataPermissions (shield enabled true/false, pioneer enabled true/false) - defaultReturn: {shield: true, pioneer: false} - parameters: [] - returns: - - $ref: dataPermissionsObject + # - name: getDataPermissions + # type: function + # async: true + # description: object of current dataPermissions with keys shield, pioneer, telemetry, 'ok' + # defaultReturn: {shield: true, pioneer: false, telemetry: true, alwaysPrivateBrowsing: false} + # parameters: [] + # returns: + # - $ref: dataPermissionsObject # telemetry related things - name: sendTelemetry @@ -456,11 +438,18 @@ description: | Send Telemetry using appropriate shield or pioneer methods. - Note: The payload must adhere to the `data.attributes` property in the [`shield-study-addon`](https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/dev/templates/include/telemetry/shieldStudyAddonPayload.3.schema.json) schema. That is, it must be a flat object with string keys and string values. + shield: + - `shield-study-addon` ping, requires object string keys and string values + + pioneer: + - TBD Note: - no conversions / coercion of data happens. + + Note: - undefined what happens if validation fails + - undefined what happens when you try to send 'shield' from 'pioneer' TBD fix the parameters here. @@ -472,26 +461,6 @@ defaultReturn: undefined # exception if out of policy based on config returns: - - name: calculateTelemetryPingSize - type: function - description: | - Calculate Telemetry using appropriate shield or pioneer methods. - - shield: - - Calculate the size of a ping - - pioneer: - - Calculate the size of a ping that has Pioneer encrypted data - - async: true - parameters: - - name: payload - $ref: telemetryPayload - - defaultReturn: undefined - returns: - - type: number - - name: searchSentTelemetry type: function async: true @@ -588,7 +557,7 @@ - name: ending type: object - # TODO 5.2+ + # TODO 5.1 # - name: onDataPermissionsChange # type: function # defaultReturn: {shield: true, pioneer: false} @@ -725,17 +694,7 @@ - isSetup: bool `setup` - isFirstRun: bool `setup`, based on pref - studySetup: bool `setup` the config - - seenTelemetry: array of seen telemetry. Fully populated only if studySetup.telemetry.internalTelemetryArchive is true + - seenTelemetry: object of lists of seen telemetry by bucket - prefs: object of all created prefs and their names parameters: [] - - - name: getInternalTestingOverrides - type: 'function' - async: true - description: | - Returns an object with the following keys: - studyType - to be able to test add-ons with different studyType configurations - Used to override study testing flags in getStudySetup(). - The values are set by the corresponding preference under the `extensions.${widgetId}.test.*` preference branch. - parameters: [] diff --git a/webExtensionApis/study/src/dataPermissions.js b/webExtensionApis/study/src/dataPermissions.js deleted file mode 100644 index 984b127..0000000 --- a/webExtensionApis/study/src/dataPermissions.js +++ /dev/null @@ -1,39 +0,0 @@ -const { Services } = ChromeUtils.import( - "resource://gre/modules/Services.jsm", - {}, -); -const { AddonManager } = ChromeUtils.import( - "resource://gre/modules/AddonManager.jsm", - {}, -); - -/** - * Checks to see if SHIELD is enabled for a user. - * - * @returns {Boolean} - * A boolean to indicate SHIELD opt-in status. - */ -export function isShieldEnabled() { - return Services.prefs.getBoolPref("app.shield.optoutstudies.enabled", true); -} - -/** - * Checks to see if the user has opted in to Pioneer. This is - * done by checking that the opt-in addon is installed and active. - * - * @returns {Boolean} - * A boolean to indicate opt-in status. - */ -export async function isUserOptedInToPioneer() { - const addon = await AddonManager.getAddonByID("pioneer-opt-in@mozilla.org"); - return isShieldEnabled() && addon !== null && addon.isActive; -} - -export async function getDataPermissions() { - const shield = isShieldEnabled(); - const pioneer = await isUserOptedInToPioneer(); - return { - shield, - pioneer, - }; -} diff --git a/webExtensionApis/study/src/getPingSize.js b/webExtensionApis/study/src/getPingSize.js deleted file mode 100644 index 8926783..0000000 --- a/webExtensionApis/study/src/getPingSize.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-env commonjs */ - -/** - * Calculate the size of a ping. - * - * @param {Object} payload - * The data payload of the ping. - * - * @returns {Number} - * The total size of the ping. - */ -function getPingSize(payload) { - const converter = Cc[ - "@mozilla.org/intl/scriptableunicodeconverter" - ].createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(payload)); - utf8Payload += converter.Finish(); - return utf8Payload.length; -} - -module.exports = { - getPingSize, -}; diff --git a/webExtensionApis/study/src/index.js b/webExtensionApis/study/src/index.js index 2ee9629..50955ed 100644 --- a/webExtensionApis/study/src/index.js +++ b/webExtensionApis/study/src/index.js @@ -10,7 +10,6 @@ import { utilsLogger, createLogger } from "./logger"; import makeWidgetId from "./makeWidgetId"; import * as testingOverrides from "./testingOverrides"; -import * as dataPermissions from "./dataPermissions"; ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); @@ -272,11 +271,6 @@ this.study = class extends ExtensionAPI { return studyUtils.info(); }, - /* Object of current dataPermissions (shield enabled true/false, pioneer enabled true/false) */ - getDataPermissions: async function getDataPermissions() { - return dataPermissions.getDataPermissions(); - }, - /** Send Telemetry using appropriate shield or pioneer methods. * * shield: @@ -315,23 +309,6 @@ this.study = class extends ExtensionAPI { return studyUtils.telemetry(payload); }, - /** Calculate Telemetry using appropriate shield or pioneer methods. - * - * shield: - * - Calculate the size of a ping - * - * pioneer: - * - Calculate the size of a ping that has Pioneer encrypted data - * - * @param {Object} payload Non-nested object with key strings, and key values - * @returns {Promise} The total size of the ping. - */ - calculateTelemetryPingSize: async function calculateTelemetryPingSize( - payload, - ) { - return studyUtils.calculateTelemetryPingSize(payload); - }, - /** Search locally stored telemetry pings using these fields (if set) * * n: @@ -471,10 +448,6 @@ this.study = class extends ExtensionAPI { async getInternals() { return studyUtils._internals; }, - - getInternalTestingOverrides: async function getInternalTestingOverrides() { - return testingOverrides.getInternalTestingOverrides(widgetId); - }, }, }; } diff --git a/webExtensionApis/study/src/studyTypes/pioneer.js b/webExtensionApis/study/src/studyTypes/pioneer.js deleted file mode 100644 index 7096dfe..0000000 --- a/webExtensionApis/study/src/studyTypes/pioneer.js +++ /dev/null @@ -1,344 +0,0 @@ -/* eslint-env commonjs */ -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(Pioneer)" }]*/ - -import { utilsLogger } from "../logger"; -import * as dataPermissions from "../dataPermissions"; -import { getPingSize } from "../getPingSize"; - -const { Services } = ChromeUtils.import( - "resource://gre/modules/Services.jsm", - {}, -); -const { TelemetryController } = ChromeUtils.import( - "resource://gre/modules/TelemetryController.jsm", - {}, -); - -const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService( - Ci.nsIUUIDGenerator, -); - -import { - setCrypto as joseSetCrypto, - Jose, - JoseJWE, -} from "jose-jwe-jws/dist/jose-commonjs.js"; - -// The public keys used for encryption -import * as PUBLIC_KEYS from "./pioneer.public_keys.json"; - -const PIONEER_ID_PREF = "extensions.pioneer.cachedClientID"; - -const EVENTS = { - INELIGIBLE: "ineligible", - EXPIRED: "expired", - USER_DISABLE: "user-disable", - ENDED_POSITIVE: "ended-positive", - ENDED_NEUTRAL: "ended-neutral", - ENDED_NEGATIVE: "ended-negative", -}; - -// Make crypto available and make jose use it. -Cu.importGlobalProperties(["crypto"]); -joseSetCrypto(crypto); - -/** - * @typedef {Object} Config - * @property {String} studyName - * Unique name of the study. - * - * @property {String?} telemetryEnv - * Optional. Which telemetry environment to send data to. Should be - * either ``"prod"`` or ``"stage"``. Defaults to ``"prod"``. - */ - -/** - * Utilities for making Pioneer Studies. - */ -class PioneerUtils { - /** - * @param {Config} config Object with Pioneer-related configuration as specified above - */ - constructor(config) { - this.config = config; - this.encrypter = null; - this._logger = null; - } - - /** - * @returns {Object} A public key - */ - getPublicKey() { - const env = this.config.telemetryEnv || "prod"; - return PUBLIC_KEYS[env]; - } - - /** - * @returns {void} - */ - setupEncrypter() { - if (this.encrypter === null) { - const pk = this.getPublicKey(); - const rsa_key = Jose.Utils.importRsaPublicKey(pk.key, "RSA-OAEP"); - const cryptographer = new Jose.WebCryptographer(); - this.encrypter = new JoseJWE.Encrypter(cryptographer, rsa_key); - } - } - - /** - * @returns {String} Unique ID for a Pioneer user. - */ - getPioneerId() { - let id = Services.prefs.getCharPref(PIONEER_ID_PREF, ""); - - if (!id) { - // generateUUID adds leading and trailing "{" and "}". strip them off. - id = generateUUID() - .toString() - .slice(1, -1); - Services.prefs.setCharPref(PIONEER_ID_PREF, id); - } - - return id; - } - - /** - * @private - * @param {String} data The data to encrypt - * @returns {String} The encrypted data - */ - async encryptData(data) { - this.setupEncrypter(); - return this.encrypter.encrypt(data); - } - - /** - * Constructs a payload object with encrypted data. - * - * @param {String} schemaName - * The name of the schema to be used for validation. - * - * @param {int} schemaVersion - * The version of the schema to be used for validation. - * - * @param {Object} data - * An object containing data to be encrypted and submitted. - * - * @returns {Object} - * A Telemetry payload object with the encrypted data. - */ - async buildEncryptedPayload(schemaName, schemaVersion, data) { - const pk = this.getPublicKey(); - - return { - encryptedData: await this.encryptData(JSON.stringify(data)), - encryptionKeyId: pk.id, - pioneerId: this.getPioneerId(), - studyName: this.config.studyName, - schemaName, - schemaVersion, - }; - } - - /** - * Encrypts the given data and submits a properly formatted - * Pioneer ping to Telemetry. - * - * @param {String} schemaName - * The name of the schema to be used for validation. - * - * @param {int} schemaVersion - * The version of the schema to be used for validation. - * - * @param {Object} data - * A object containing data to be encrypted and submitted. - * - * @param {Object} options - * An object with additional options for the function. - * - * @param {Boolean} options.force - * A boolean to indicate whether to force submission of the ping. - * - * @returns {String} - * The ID of the ping that was submitted - */ - async submitEncryptedPing(schemaName, schemaVersion, data, options = {}) { - // If the user is no longer opted in we should not be submitting pings. - const isUserOptedIn = await dataPermissions.isUserOptedInToPioneer(); - if (!isUserOptedIn && !options.force) { - return null; - } - - const payload = await this.buildEncryptedPayload( - schemaName, - schemaVersion, - data, - ); - - const telOptions = { - addClientId: true, - addEnvironment: true, - }; - - return TelemetryController.submitExternalPing( - "pioneer-study", - payload, - telOptions, - ); - } - - /** - * Gets an object that is a mapping of all the available events. - * - * @returns {Object} - * An object with all the available events. - */ - getAvailableEvents() { - return EVENTS; - } - - /** - * Submits an encrypted event ping. - * - * @param {String} eventId - * The ID of the event that occured. - * - * @param {Object} options - * An object of options to be passed through to submitEncryptedPing - * - * @returns {String} - * The ID of the event ping that was submitted. - */ - async submitEventPing(eventId, options = {}) { - if (!Object.values(EVENTS).includes(eventId)) { - throw new Error("Invalid event ID."); - } - return this.submitEncryptedPing("event", 1, { eventId }, options); - } -} - -class PioneerStudyType { - /** - * @param {object} studyUtils The studyUtils instance from where this class was instantiated - */ - constructor(studyUtils) { - const studySetup = studyUtils._internals.studySetup; - const Config = { - studyName: studySetup.activeExperimentName, - telemetryEnv: studySetup.telemetry.removeTestingFlag ? "prod" : "stage", - }; - this.pioneerUtils = new PioneerUtils(Config); - this.schemaVersion = 3; // Corresponds to the schema versions used in https://github.com/mozilla-services/mozilla-pipeline-schemas/tree/dev/templates/telemetry/shield-study (and the shield-study-addon, shield-study-error equivalents) - } - - /** - * @returns {Promise} The ID of the event ping that was submitted. - */ - async notifyNotEligible() { - return this.notifyEndStudy(this.EVENTS.INELIGIBLE); - } - - /** - * @param {String?} eventId The ID of the event that occured. - * @returns {Promise} The ID of the event ping that was submitted. - */ - async notifyEndStudy(eventId = EVENTS.ENDED_NEUTRAL) { - return this.pioneerUtils.submitEventPing(eventId, { force: true }); - } - - /** - * @returns {Promise} Unique ID for a Pioneer user. - */ - async getTelemetryId() { - return this.pioneerUtils.getPioneerId(); - } - - /** - * @param {String} bucket The type of telemetry payload - * @param {Object} payload The telemetry payload - * @returns {Promise} The ID of the ping that was submitted - */ - async sendTelemetry(bucket, payload) { - const schemaName = bucket; - return this._telemetry(schemaName, this.schemaVersion, payload); - } - - /** - * Encrypts the given data and submits a properly formatted - * Pioneer ping to Telemetry. - * - * @param {String} schemaName - * The name of the schema to be used for validation. - * - * @param {int} schemaVersion - * The version of the schema to be used for validation. - * - * @param {Object} payload - * A object containing data to be encrypted and submitted. - * - * @returns {Promise} The ID of the ping that was submitted - * @private - */ - async _telemetry(schemaName, schemaVersion, payload) { - const pingId = await this.pioneerUtils.submitEncryptedPing( - schemaName, - schemaVersion, - payload, - ); - if (pingId) { - utilsLogger.debug( - "Pioneer Telemetry sent (encrypted)", - JSON.stringify(payload), - ); - } else { - utilsLogger.debug( - "Pioneer Telemetry not sent due to privacy preferences", - JSON.stringify(payload), - ); - } - return pingId; - } - - /** - * Calculate the size of a ping. - * - * @param {String} bucket The type of telemetry payload - * - * @param {Object} payload - * The data payload of the ping. - * - * @returns {Promise} - * The total size of the ping. - */ - async getPingSize(bucket, payload) { - const schemaName = bucket; - return this.getEncryptedPingSize(schemaName, this.schemaVersion, payload); - } - - /** - * Calculate the size of a ping that has Pioneer encrypted data. - * - * @param {String} schemaName - * The name of the schema to be used for validation. - * - * @param {int} schemaVersion - * The version of the schema to be used for validation. - * - * @param {Object} data - * An object containing data to be encrypted and submitted. - * - * @returns {Promise} - * The total size of the ping. - */ - async getEncryptedPingSize(schemaName, schemaVersion, data) { - return getPingSize( - await this.pioneerUtils.buildEncryptedPayload( - schemaName, - schemaVersion, - data, - ), - ); - } -} - -export default PioneerStudyType; diff --git a/webExtensionApis/study/src/studyTypes/pioneer.public_keys.json b/webExtensionApis/study/src/studyTypes/pioneer.public_keys.json deleted file mode 100644 index 07e8f55..0000000 --- a/webExtensionApis/study/src/studyTypes/pioneer.public_keys.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "stage": { - "id": "pioneer-20170905", - "key": { - "e": "AQAB", - "kty": "RSA", - "n": - "3nI-DQ7NoUZCvT348Vi4JfGC1h6R3Qf_yXR0dKM5DmwsuQMxguce6sZ28GWQHJjgbdcs8nTuNQihyVtr9vLsoKUVSmPs_a3QEGXEhTpuTtm7cCb_7HyAlwGtysn2AsdElG8HsDFWlZmiDaHTrTmdLnuk-Z3GRg4nnA4xs4vvUuh0fCVIKoSMFyt3Tkc6IBWJ9X3XrDEbSPrghXV7Cu8LMK3Y4avy6rjEGjWXL-WqIPhiYJcBiFnCcqUCMPvdW7Fs9B36asc_2EQAM5d7BAiBwMjoosSyU6b4JGpI530c3xhqLbX00q1ePCG732cIwp0-bGWV_q0FpQX2M9cNv2Ax4Q" - } - }, - "prod": { - "id": "pioneer-20170905", - "key": { - "e": "AQAB", - "kty": "RSA", - "n": - "_uqWswIJpR-cFdwwtNdAI_B_0sPIyQyBy6hiiQ0GKLF2k1PkN6RaxtbZK8v1_BriYtEgWn3hNzJNbKBWBMFtF5-8OfvxH-hgIIeDmRmeHmynLBBCDVf2HAZYaDXJiM7s6LBubDuoPDc3Ovoj287W7E4LgzsBS0wo3ARIwlKn6x0Dj5tu6CQ5r3t0GKZoSFkiVZA7nke-VC55nlDacIIYAqkMX0dzsBaCRmf2C5JJTP-K14iRLB5VFGZ_vnoZ-Wi1BGRV2TNRl3xl0lFJIcPklFpU3hsnRPiF4y7kenU6OIhJVQMqX1CtCF698k7SFCYJt7r1ymWJE-tv0ZwF9b1MFw" - } - } -} diff --git a/webExtensionApis/study/src/studyTypes/shield.js b/webExtensionApis/study/src/studyTypes/shield.js deleted file mode 100644 index 8a4abb2..0000000 --- a/webExtensionApis/study/src/studyTypes/shield.js +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-env commonjs */ - -import { getPingSize } from "../getPingSize"; - -const { TelemetryController } = ChromeUtils.import( - "resource://gre/modules/TelemetryController.jsm", - null, -); -const CID = ChromeUtils.import("resource://gre/modules/ClientID.jsm", {}); -// ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); - -// eslint-disable-next-line no-undef -// const { ExtensionError } = ExtensionUtils; - -class ShieldStudyType { - /** - * @param {object} studyUtils The studyUtils instance from where this class was instantiated - */ - constructor(studyUtils) { - // console.log("studyUtils", studyUtils); - } - - /** - * @returns {Promise} The telemetry client id - */ - async getTelemetryId() { - const id = TelemetryController.clientID; - /* istanbul ignore next */ - if (id === undefined) { - return CID.ClientIDImpl._doLoadClientID(); - } - return id; - } - - /** - * @param {String} bucket The type of telemetry payload - * @param {Object} payload The telemetry payload - * @returns {Promise} The ID of the ping that was submitted - */ - async sendTelemetry(bucket, payload) { - const telOptions = { addClientId: true, addEnvironment: true }; - return TelemetryController.submitExternalPing(bucket, payload, telOptions); - } - - /** - * Calculate the size of a ping. - * - * @param {String} bucket The type of telemetry payload - * - * @param {Object} payload - * The data payload of the ping. - * - * @returns {Promise} - * The total size of the ping. - */ - async getPingSize(bucket, payload) { - return getPingSize(payload); - } -} - -export default ShieldStudyType; diff --git a/webExtensionApis/study/src/studyUtils.js b/webExtensionApis/study/src/studyUtils.js index 08b2c25..4757616 100644 --- a/webExtensionApis/study/src/studyUtils.js +++ b/webExtensionApis/study/src/studyUtils.js @@ -5,8 +5,6 @@ import sampling from "./sampling"; import { utilsLogger } from "./logger"; import makeWidgetId from "./makeWidgetId"; -import ShieldStudyType from "./studyTypes/shield"; -import PioneerStudyType from "./studyTypes/pioneer"; /* * Supports the `browser.study` webExtensionExperiment api. @@ -51,6 +49,11 @@ const { ExtensionUtils } = ChromeUtils.import( const { ExtensionError } = ExtensionUtils; // telemetry utils +const CID = ChromeUtils.import("resource://gre/modules/ClientID.jsm", {}); +const { TelemetryController } = ChromeUtils.import( + "resource://gre/modules/TelemetryController.jsm", + null, +); const { TelemetryEnvironment } = ChromeUtils.import( "resource://gre/modules/TelemetryEnvironment.jsm", null, @@ -175,7 +178,7 @@ class StudyUtils { * - isSetup: bool `setup` * - isFirstRun: bool `setup`, based on pref * - studySetup: bool `setup` the config - * - seenTelemetry: array of seen telemetry. Fully populated only if studySetup.telemetry.internalTelemetryArchive is true + * - seenTelemetry: object of lists of seen telemetry by bucket * - prefs: object of all created prefs and their names * - endingRequested: string of ending name * - endingReturns: object with useful ending instructions @@ -222,7 +225,11 @@ class StudyUtils { isSetup: false, isEnding: false, isEnded: false, - seenTelemetry: [], + seenTelemetry: { + "shield-study": [], + "shield-study-addon": [], + "shield-study-error": [], + }, prefs: { firstRunTimestamp: `shield.${widgetId}.firstRunTimestamp`, }, @@ -261,15 +268,6 @@ class StudyUtils { throw new ExtensionError("StudyUtils is already setup"); } guard.it("studySetup", studySetup, "(in studySetup)"); - this._internals.studySetup = studySetup; - - // Different study types treat data and configuration differently - if (studySetup.studyType === "shield") { - this.studyType = new ShieldStudyType(this); - } - if (studySetup.studyType === "pioneer") { - this.studyType = new PioneerStudyType(this); - } function getVariationByName(name, variations) { if (!name) return null; @@ -296,6 +294,7 @@ class StudyUtils { utilsLogger.debug(`setting up: variation ${variation.name}`); this._internals.variation = variation; + this._internals.studySetup = studySetup; this._internals.isSetup = true; // isFirstRun? ever seen before? @@ -319,7 +318,6 @@ class StudyUtils { */ reset() { this._internals = this._createInternals(); - this.studyType = null; this.resetFirstRunTimestamp(); } @@ -401,7 +399,12 @@ class StudyUtils { * @returns {string} - the telemetry client ID */ async getTelemetryId() { - return this.studyType.getTelemetryId(); + const id = TelemetryController.clientID; + /* istanbul ignore next */ + if (id === undefined) { + return CID.ClientIDImpl._doLoadClientID(); + } + return id; } /** @@ -429,16 +432,6 @@ class StudyUtils { shieldId: this.getShieldId(), delayInMinutes: this.getDelayInMinutes(), }; - const now = new Date(); - const diff = Number(now) - studyInfo.firstRunTimestamp; - utilsLogger.debug( - "Study info date information: now, new Date(firstRunTimestamp), firstRunTimestamp, diff (in minutes), delayInMinutes", - now, - new Date(studyInfo.firstRunTimestamp), - studyInfo.firstRunTimestamp, - diff / 1000 / 60, - studyInfo.delayInMinutes, - ); guard.it("studyInfoObject", studyInfo, "(in studyInfo)"); return studyInfo; } @@ -464,7 +457,8 @@ class StudyUtils { weightedVariations, fraction = null, ) { - // this is the standard arm choosing method, used by both shield and pioneer studies + // this is the standard arm choosing method + // TODO, allow 'pioneer' algorithm if (fraction === null) { // hash the studyName and telemetryId to get the same branch every time. const clientId = await this.getTelemetryId(); @@ -739,39 +733,32 @@ class StudyUtils { } utilsLogger.debug(`telemetry: ${JSON.stringify(payload)}`); - let pingId; + // IF it's a shield-study or error ping, which are few in number + if (bucket === "shield-study" || bucket === "shield-study-error") { + this._internals.seenTelemetry[bucket].push(payload); + } // during development, don't actually send if (!this.telemetryConfig.send) { utilsLogger.debug("NOT sending. `telemetryConfig.send` is false"); - pingId = false; - } else { - pingId = await this.studyType.sendTelemetry(bucket, payload); - } - - // Store a copy of the ping if it's a shield-study or error ping, which are few in number, or if we have activated the internal telemetry archive configuration - if ( - bucket === "shield-study" || - bucket === "shield-study-error" || - this.telemetryConfig.internalTelemetryArchive - ) { - this._internals.seenTelemetry.push({ id: pingId, payload }); + return false; } - return pingId; + const telOptions = { addClientId: true, addEnvironment: true }; + return TelemetryController.submitExternalPing(bucket, payload, telOptions); } /** * Validates and submits telemetry pings from the add-on; mostly from * webExtension messages. - * @param {Object} payload - the data to send as part of the telemetry packet + * @param {Object} data - the data to send as part of the telemetry packet * @returns {Promise|boolean} - see StudyUtils._telemetry */ - async telemetry(payload) { + async telemetry(data) { this.throwIfNotSetup("telemetry"); - utilsLogger.debug(`telemetry ${JSON.stringify(payload)}`); + utilsLogger.debug(`telemetry ${JSON.stringify(data)}`); const toSubmit = { - attributes: payload, + attributes: data, }; return this._telemetry(toSubmit, "shield-study-addon"); } @@ -784,25 +771,6 @@ class StudyUtils { telemetryError(errorReport) { return this._telemetry(errorReport, "shield-study-error"); } - - /** Calculate Telemetry using appropriate shield or pioneer methods. - * - * shield: - * - Calculate the size of a ping - * - * pioneer: - * - Calculate the size of a ping that has Pioneer encrypted data - * - * @param {Object} payload Non-nested object with key strings, and key values - * @returns {Promise} The total size of the ping. - */ - async calculateTelemetryPingSize(payload) { - this.throwIfNotSetup("calculateTelemetryPingSize"); - const toSubmit = { - attributes: payload, - }; - return this.studyType.getPingSize(toSubmit, "shield-study-addon"); - } } // TODO, use the usual es6 exports diff --git a/webExtensionApis/study/src/telemetry.js b/webExtensionApis/study/src/telemetry.js index 5cef114..6acceb3 100644 --- a/webExtensionApis/study/src/telemetry.js +++ b/webExtensionApis/study/src/telemetry.js @@ -53,6 +53,8 @@ async function searchTelemetryArchive(TelemetryArchive, searchTelemetryQuery) { return Promise.all(pingData); } +// TODO pings report, from the utility add-on + module.exports = { searchTelemetryArchive, }; diff --git a/webExtensionApis/study/src/testingOverrides.js b/webExtensionApis/study/src/testingOverrides.js index 1b85132..8769a4e 100644 --- a/webExtensionApis/study/src/testingOverrides.js +++ b/webExtensionApis/study/src/testingOverrides.js @@ -25,10 +25,3 @@ export function listPreferences(widgetId) { `extensions.${widgetId}.test.expired`, ]; } - -export function getInternalTestingOverrides(widgetId) { - const internalTestingOverrides = {}; - internalTestingOverrides.studyType = - Preferences.get(`extensions.${widgetId}.test.studyType`) || null; - return internalTestingOverrides; -}